Design Patterns in Automated Testing
Podcast: Play in new window | Download (51.7MB) | Embed
Subscribe: Apple Podcasts | Spotify | Email | RSS | More
Test code is code. This cannot be emphasized enough. So many developers don’t treat tests as real code that needs to be designed with longevity in mind. Just like in other areas of the code patterns have emerged to aid in the design of tests.
To have successful automation in testing you need to design your test code correctly. Testing design patterns are solutions or ways of setting up code that have been successful over and over again. They were created to solve common problems are generally considered to be best practices in writing the code that tests other code.
This is a high level overview of some of the design patterns used in automated testing. Since test code is code these are similar to coding design patterns. The implementation details will vary between languages and frameworks. Use this information to understand the concepts behind these patterns when you are learning or implementing them in your own testing.
This is a simple interface for more complex code. Likely this code is an API or class library. You may not need everything that is provided by a particular code.
Only the features needed get exposed through the facade. This improves ease of use and maintainability of the code. It also reduces dependencies on external code through a unified interface. By simplifying the way the code is used you can prevent misuse.
A fluent interface is a way of implementing and API interface in order to provide more readable code. It uses method cascading to clearly express what the code does. Method cascading is a technique where each method returns an object. Then methods can be chained together to form a single statement.
For testing purposes a facade would only expose what needs to be tested. This reduces the chances of testing the wrong thing. It also prevents testing from happening on unfinished (un-flagged) features.
Factories wrap the creation of objects. They handle finding or creating the necessary dependencies of the class being instantiated. This reduces the dependencies on concrete classes.
It’s used to create objects based on specific rules. There may be many classes implementing an interface. The calling code doesn’t need to know all the rules for instantiating an object so it passes that responsibility to the factory. The factory knows which classes are needed to instantiate an object.
In automated testing the factory pattern helps to prevent repetition. All the instantiation code is on one place making changes easier. Adding more conditions is simplified because you only have to change the factory.
A singleton pattern limits a class to only be instantiated once or in one object. It should have a parameterless constructor. The once instance will be used globally throughout the scope in which it is created.
Singletons are used if creating a class requires a lot of resources. This way the class is only created once and saving the recreation costs. Lazy initialization means the class should only be created when first needed.
For testing purposes a singleton may be a large class or repository. You only need to create it once for all of your tests. This speeds the testing process.
Null Object Pattern
A null object is an object with empty values for it’s properties. This is not a null value for the object as a whole. Internal objects or parameters exist but have empty or default values. When implementing an interface the methods return null or do nothing.
The idea is to reduce complexity by not requiring null checks on objects before using them. Methods and properties can be invoked without fear of a null reference exception. This improves the predictability of objects. It also reduces the amount of unnecessary error handling in the code. Nested objects still exist though they may be empty or null.
The biggest benefit to testing is the ability to call nested objects even if they don’t exist. The most annoying thing about a null reference exception is not knowing where the null exists on multiply nested objects. This pattern reduces that frustration by allowing you to call the nested object without fear of the exception.
The strategy pattern allows the behavior of your code to be determined at runtime. It defines a group of algorithms and encapsulates each one. This makes the algorithms interchangeable. You only have to understand the strategy to maintain the test code.
There are several objects and classes involved in this pattern. First and interface exists that is common to all the algorithms. The interface in called to use the algorithm. Which method or algorithm is determined by the concrete strategy.
The concrete strategy implements the algorithm from the interface. The context wraps the calls to the concrete strategy. It is dependant on the strategy interface. It may serve as an interface to access data or other areas.
The strategy pattern allows for decoupled validation. The tests are more easily extendable. The strategy used may be based on many different factors. Multiple tests can use the same strategy in different areas.
Page Object Pattern
The page object abstracts the HTML of a page. It creates an interface for manipulating elements without searching for them in the HTML. An object repository of elements is also created.
Basically the page object wraps everything (elements, actions, etc.) on a page into a single object. The page object contains the elements of the page and the actions that can be done on a page. This includes things like navigation and search functions. It also includes an element map which has all the element properties and their location.
A class of tests is created for each HTML page. This class contains all the tests related to that page. It will only have one instance of a page object. So within the scope of the test class the page object is a singleton.
This pattern separates the HTML from the test logic. This increases the maintainability and reusability of test code. It also hides the test logic making tests more readable to non developers and business people.
Observer Design Pattern
The observer pattern emphasizes a one-to-many relationship between objects. When the state of one object, the subject, changes it’s dependents, observers, are notified. This allows for loose coupling of objects and classes. Updates and data are easily sent to dependent objects.
Adding a new observer doesn’t require changes to the parent object. This means you can add or remove observers as needed. However there is no order of priority in observer notification.
Subject and object classes and interfaces are required for using this pattern. A subject class is required for adding and removing observers. This also includes the various notification methods for observer classes. It’s interface is how observer objects and classes will register as observers. An observer is a class that registers with the subject’s interface. This can be any class that implements the observer interface. The observer interface contains the methods on the observer that are called when the subject changes state.
This pattern improves test automation through use of the Open/Closed Principle from SOLID. Test code is open for extension but closed for modification. The pattern allows for the creation of extendable tests.
The Elements of Computing Systems
The book closes out talking about higher level or human readable languages and systems. Chapter 9 introduces a high level language called Jack. It is a simple general purpose object oriented language. Chapters 10 and 11 focus on building a compiler to translate the code written in Jack to machine language that the virtual hardware built in the early chapters can understand. Finally chapter 12 completes the puzzle by working through building a basic operating system to run on the hardware.
Tricks of the Trade
Will didn’t write anything.
Tricks of the Trade:
Try to find situations where things are messed up and write automatic tests for or integrate with the code concerned. You will learn a lot by doing this. You will for example recognize patterns which cause problems, so that you can avoid them yourself when writing new code.