Beyond Scripting – A Thoughtful Approach to Test Automation
In software development projects, we often encounter challenges related to code quality. While most teams focus on production code, it’s crucial to recognize that test automation code deserves equal attention. This perspective was highlighted in an insightful article by Kim Filiatrault, who wrote about the often-overlooked distinction between production code and test code. During the #Testμ conference, Paul Grizzaffi emphasized we should treat test automation as software and prioritize its quality. Test code quality goes beyond code review and static analysis using tools like SonarCloud; it encompasses the entire architecture design.
While working with Guidewire clients like SGI and using the CenterTest framework, we are following specific rules to ensure high-quality, maintainable test automation code.
In this article, we’ll explore a four-step process to organize your test automation code effectively. As we go through each step, we’ll highlight how our approach naturally adheres to the SOLID principles of object-oriented design:
- Single Responsibility Principle (SRP)
- Open-Closed Principle (OCP)
- Liskov Substitution Principle (LSP)
- Interface Segregation Principle (ISP)
- Dependency Inversion Principle (DIP)
We will discuss how our design and architecture (approach) adhere to SOLID principles to eliminate traditional scripting in favor of a highly maintainable and extendable reusable class structure that provides a complete foundation for InsuranceSuite test automation in CenterTest, offering several key advantages:
- Increased efficiency: By eliminating repetitive scripting, we reduce the time and effort required to create and maintain tests.
- Enhanced scalability: Our reusable class structure allows for easy expansion of test coverage as your application grows.
- Improved reliability: Well-organized, modular code is less prone to errors and easier to debug.
- Better collaboration: A clear, structured approach makes it easier for team members to understand and contribute to the test suite.
- Faster onboarding: New team members can quickly grasp and work with a well-designed test automation framework.
- Long-term cost savings: While initial setup may take more time, the ease of maintenance and expansion leads to significant time and resource savings over the project lifecycle.
This comprehensive approach not only improves the quality of your test automation but also aligns it more closely with software engineering best practices, ultimately leading to more robust and reliable InsuranceSuite implementations.
Understanding CenterTest and Page Object Models (POMs)
Before we dive into our organization process, it’s crucial to understand a key aspect of our testing framework. Using CenterTest, we generate Page Object Model (POM) classes directly from Guidewire source code. These POM classes contain only page elements, providing a clean separation between the structure of our pages and their behavior.
It’s important to note that these POM classes should remain focused solely on accessing elements, and I strongly encourage everyone to avoid connecting POMs directly with functional pieces of code. This separation adheres to the Single Responsibility Principle (SRP) from SOLID, ensuring that each class has only one reason to change.
Pre-step: The Monolithic Test
Let’s look at a typical example of how many automation efforts begin. This is more of a script than proper test code – it covers all steps needed to create a policy using Guidewire PolicyCenter, but with no real design, just a pure flow of actions. While it uses CenterTest, it lacks proper structure and doesn’t apply object-oriented principles.
Let us look at a typical example of how many automation efforts begin, which is often more scripting than proper test coding. The example below covers all steps needed to create a policy using Guidewire PolicyCenter, but with no real design, just a pure flow of actions. While it runs in CenterTest, it lacks proper structure and does not apply object-oriented principles.
This is how a lot of automation starts out: functional, but without consideration for long-term maintainability or scalability. Here’s an example that illustrates the challenges we’ll address in the following steps:
This monolithic test, while functional, presents several challenges in terms of readability, maintainability and reusability. We can see only the beginning because the whole script has around 600 lines of code with complete flow in Guidewire PolicyCenter. Now, we’ll break down this test and reorganize it for better efficiency and clarity.
Step 1: Divide the Monolithic Test into Methods
The first step in our organization process is to break monoliths into smaller, more manageable methods, adhering to the SRP .
Initially, our test might encompass account creation, policy creation and policy verification all within a single method. Policy creation itself is a complex wizard process where we need to go through steps like Policy Info, Drivers, Vehicles, Coverages, Quote, and Payment. By dividing the whole test into separate methods like createAccount(), setPolicyInfo(), setDrivers(), setVehicle(), setCoverages(), quote(), processPayment(), and verifyPolicyDetails(), we introduce a level of modularity that brings several benefits and aligns with the SRP.
This division improves readability, with each method having a clear, singular purpose, making it easier for other developers to understand the test’s flow. Additionally, it enhances maintainability. If there is a change in the account creation process, for example, you only need to update the createAccount() method, leaving the rest of the test intact.
These smaller methods promote reusability, as they can be easily called from other test cases, reducing code duplication and ensuring consistency across your test suite.
This is how it might look in practice.
This restructuring lays the groundwork for more sophisticated organization in the subsequent steps and ensures that each method has a single, clear responsibility.
Step 2: Organize Methods into Reusable Classes
As our test suite expands, we will find ourselves with an increasing number of these smaller, focused methods. While this is a significant improvement over our initial monolithic tests, it presents a new challenge with managing many methods within a single test class. This is where our second step comes in – organizing these methods into reusable classes, keeping in mind the Open-Closed Principle (OCP).
The key here is to group related functionality together, while ensuring our classes are open for extension but closed for modification. For instance, we can move all methods related to account creation into CreateAccount class, while we can place those dealing with policies in a CreatePolicy class.
This organization not only makes our code more logically structured; it also significantly enhances its reusability and extensibility. Furthermore, we use naming conventions according to behaviors we are dealing with. This allows us to use CenterTest Narrative – BDD reporting provided with the framework.
Let’s observe the outcome of that refactoring.
This structure allows for easier expansion of our functionality without modifying existing classes, adhering to the OCP. If we need to add new account types or policy operations, we can extend these base classes rather than modifying them directly.
Step 3: Create Extended Classes
Next, we may encounter scenarios where we need specialized functionality that builds upon our existing classes. This is where the power of inheritance comes into play, guided by the Liskov Substitution Principle (LSP). By creating extended classes, we can add or change functionality for specific purposes without altering our base classes, ensuring that objects of our subclasses can be used interchangeably with objects of the superclass.
Let’s consider a scenario where we need to create a business account, which requires additional information compared to a personal account. To do that, we can extend our CreateAccount class to handle this using CreateBusinessAccount:
Now we can create a test that uses these specialized classes:
This approach allows us to handle specialized scenarios while maintaining the structure and benefits of our base classes. In particular, it promotes code reuse and keeps our test code DRY (Don’t Repeat Yourself). It ensures that we can use our extended classes wherever their base classes are expected, adhering to the LSP.
While extended classes offer great flexibility, it’s important to use them judiciously. Overuse of inheritance can lead to a complex class hierarchy that’s difficult to maintain. Always consider whether composition might be a better alternative to inheritance for your specific use case.
For instance, instead of creating a CreateBackdatedPolicy class that inherits from CreatePolicy, you might handle it with a simple data condition. This approach often provides more flexibility and avoids potential issues with the Liskov Substitution Principle.
Step 4: Create a Factory for Easy Access to Reusables
As our test suite grows more complex with various types of accounts and policies, managing the creation of these objects can become challenging. This is where the Factory pattern comes in handy, allowing us to apply both the Interface Segregation Principle (ISP) and the Dependency Inversion Principle (DIP). By implementing a factory class, we can centralize the creation of our test objects, making it easier to manage dependencies and configurations.
Let’s create a TestFactory class:
Now, we can refactor our test to use this factory:
While this factory implementation might seem simple, it adheres well to SOLID principles because of our underlying class structure:
This structure means that while our factory methods return concrete classes, these classes are part of an abstraction hierarchy. CenterTest clients working with the objects created by the factory are essentially using Scenario interface or BaseScenarioPC abstraction.
This approach adheres to DIP by depending on abstractions (Scenario interface and abstract classes) rather than concretions. It also supports ISP by providing focused factory methods that align with the segregated responsibilities defined in our Scenario interface and abstract classes.
The TestFactory approach offers several benefits:
- Centralized object creation: All test objects are created in one place, simplifying the management of dependencies and configurations.
- Improved maintainability: If we need to change how objects are created or initialized, we only need to update the factory class.
- Flexibility: It’s easy to switch between different implementations or add new types of objects without affecting existing tests.
- Reduced duplication: The factory eliminates the need to repeatedly instantiate objects in multiple test classes.
When implementing the factory pattern, consider using a Simple Factory for straightforward object creation, or an Abstract Factory when you need to create families of related objects. In our case, we will use an Abstract Factory to create different types of accounts (personal, business) along with their associated policies.
This approach not only centralizes object creation but also makes it easier to add new types of accounts or policies in the future, supporting the Open-Closed Principle.
By implementing these four steps – dividing monolithic tests (SRP), organizing methods into extensible classes (OCP), creating substitutable extended classes (LSP), and using a factory with segregated interfaces (ISP and DIP) – we’ve significantly improved the structure and maintainability of our test automation code while adhering to SOLID principles.
From Refactoring to Upfront Design
As you become more familiar with these principles and practices, you’ll find that your approach to test automation evolves. What starts as a process of refactoring existing code gradually transforms into a methodology for designing new tests from the ground up.
The more you apply these steps to reorganize your existing test suite, the more intuitive it becomes to structure new tests in this modular, maintainable way from the outset. You’ll start to:
- Naturally, think in terms of small, focused methods with single responsibilities.
- Instinctively organize related functionality into cohesive classes.
- Anticipate potential variations and design for extensibility from the start.
- Implement factories and other design patterns proactively, rather than reactively.
This shift from reactive refactoring to proactive design is a significant milestone in your journey towards high-quality test automation. It means you’re not just fixing problems, but preventing them before they occur. Moreover, your test suites become more robust, flexible, and maintainable right from their inception.
Remember, the goal is continuous improvement. As you internalize these principles, you’ll find yourself writing cleaner, more efficient code from the very beginning of each new test automation project. In the long run, this proactive approach saves time, reduces technical debt, and results in a more enjoyable and productive testing process for your entire team.
Design for Guidewire Testing
Modern Integrated Development Environments (IDEs) are powerful allies in improving your Guidewire InsuranceSuite test automation. These tools make refactoring a breeze – you can rename methods, extract classes, and reorganize code with just a few clicks. This means you can easily enhance your existing tests without a complete rewrite.
When it comes to Guidewire testing, you’re not starting from scratch. CenterTest provides you with a comprehensive set of reusable, extendable classes that cover out-of-the-box product lines and processes. This means you don’t need to build a foundation – it’s already there, ready for you to use.
These pre-built classes in CenterTest cover insurance operations, policy management, and both common and complex workflows in Guidewire applications. They’re designed to be easily customizable, allowing you to adapt them to your specific implementation without reinventing the wheel.
For instance, if you need to test a personal auto policy creation with your own specific coverages, you don’t need to write all the basic policy handling code. CenterTest already provides classes for policy management. You can simply use these classes and focus on extending them with custom logic which handles customer coverages.
The goal isn’t to build on top of CenterTest, but to leverage what it provides and customize where needed. This approach allows you to focus on your unique testing requirements rather than setting up basic infrastructure. By utilizing CenterTest’s ready-made components and applying good design principles, you can create a test suite that’s both comprehensive and maintainable, tailored to your Guidewire implementation within hours.
Conclusion
As we wrap up our exploration of test automation code organization, let’s take a moment to reflect on the practical impacts of what we’ve discussed.
We started with a common problem: monolithic test cases that are hard to read, maintain, and expand. By breaking these down into smaller, focused methods and then organizing them into reusable classes, we’ve created a structure that’s much easier to work with day-to-day. Imagine the time saved when you need to update a test because the login process has changed – now you only need to modify one method in one class, rather than updating dozens of test cases.
Introducing extended classes gives us the flexibility to handle special cases without cluttering our base classes. This means we can include different kinds of accounts or policies without rewriting existing tests or complicating our core classes.
Our TestFactory approach might seem like an extra layer at first but consider how it simplifies test writing. New team members can get up to speed quickly, creating test objects with a few simple method calls rather than needing to underFor Gudiewire tand the intricacies of object initialization.
These improvements are not just theoretical – they translate to real-world benefits:
- Faster bug fixes: When a test fails, it’s much easier to pinpoint the issue when each method has a clear, single purpose.
- Quicker test development: With a library of reusable components, creating new tests becomes more about assembling existing pieces than writing from scratch.
- Easier maintenance: When the system under test changes, our modular approach means we’re likely only updating a small, specific part of our test code.
- Better collaboration: Team members can work on different parts of the test suite without stepping on each other’s toes, thanks to our clear separation of concerns.
As you head back to your projects, consider starting small. Maybe begin by breaking down one large test case, or creating a reusable class for a common operation like login. You don’t need to refactor your entire test suite overnight. Instead, incrementally apply these principles as you write new tests and update existing ones.
Remember, the goal isn’t perfection, but improvement. Each step towards better-organized test code is a step towards a more maintainable, reliable test suite. And that means better software, happier teams, and more satisfied end-users.
So, take these ideas, adapt them to your specific needs, and start building a test automation framework that not only checks your software but helps drive its quality forward. Your future self (and your team) will thank you.
Special thanks to Dariusz Czajkiewicz and the Kimputing team: Raivis Sarkovskis, Kim Filiatrault , and Duncan Guignet, for their contributions to this article.
Arek Frankowski
Senior Software Architect
Watch or read our other posts at Kimputing Blogs. You’ll find everything from Automated testing to CenterTest, Guidewire knowledge to general interest. We’re trying to help share our knowledge from decades of experience.