Practical complex data for unit testing.
Unit testing is a long-established and essential part of the software development process, especially when using .NET. The basic premise, if you don’t know, is to test the public practices of the classes so that they work as expected.
Unit tests follow 3As: Take care, Act, And Claim. In other words, set the conditions for the test, run the code and finally check that it worked as expected. If so, great test! If not here’s a new product just for you!
This article takes a look at the first step in arranging the test. Although there is some work to be done during this phase, for now we are only concerned with creating the data needed to run the test.
Later, we are going to look at how to create complex test data that can be easily understood using builder patterns.
Why is it important how test data is created?
Well, looking at and understanding test data can be confusing. It takes time to follow up and update, this is especially the case where there are relationships to maintain test data. If the test data is not clear, it is easy to make mistakes and introduce bugs in the test code.
Test data that was written long ago becomes difficult to remember. Like everything else, it is fresh in the mind when it is written, but after three or six months, it becomes difficult to remember the intent of the data. Sometimes breaking the DRY (don’t repeat yourself) rule and just repeating the test data is easy and very attractive. As new tests are developed, test data that is not well understood is usually copied and adapted for that purpose. This only increases the technical debt within the test data which makes it difficult to maintain the entire project.
Configure test data.
Mostly, if not all depends on code parameters or run data. So when it comes to unit testing and emphasizing that the code works, it usually requires some amount of data to run the test. It can range from a simple value type parameter to a complex object model, or a large set of related data. There are different ways to create unit test data and we will take a brief look at some of them.
Test data can be created directly in unit tests, passed in line tests, loaded from files, or created for use in memory databases. I recommend these memory databases for unit testing. A separate database hosted outside the test execution process will be considered integration testing.
Adding test data with the xUnit unit test framework.
I use xUnit to handle and run my unit tests. It is a popular, free, open source, and community-focused unit testing framework that provides some useful ways to manage our test data.
Theory unit test.
Theory unit tests allow the same test to work on a set of parametric data. Data is transferred using the inline data attribute for many different cases as needed. This test code contains DRY and eliminates the need to repeat test logic for different data values.
In cases where test data is complex, xUnit also supports class data and member data attributes to load test data in one way or another. xUnit is scalable and can be used to write custom data properties, for example, to load test data from files or from databases.
The final xUnit feature is the fixture class for viewing. Fixtures allow the sharing of setup and cleanup codes for all tests in one class or even multiple test classes. This is a great place to create complex test data and a great place to create a memory database for those tests that are needed.
For our own development, we often acquire MySQL Server through Antivirus Framework, and for testing we use MySQLight In Memory Database to copy the database layer. We use this approach to start the database in memory that we need.
A clean library of auto-fixtures for creating and generating test data. The creators described it this way:
AutoFixtures is an open source library for .NET designed to minimize the ‘configuration’ phase of your unit tests in order to maximize maintenance. Its main goal is to allow developers to focus on how to configure a test scenario, making it easier to create object graphs containing test data.
We don’t use auto-fixtures to create our test data in this example, but it’s worth noting because it can speed up the test writing process.
How builder patterns can help create complex data.
As seen earlier, such problems arise when test data is complex. Thankfully, there is an approach that we can use to help us and that has been around for a long time. Builder pattern is a well-understood design pattern in software development. It allows the construction of complex objects in stages and can be used to create different representations of complex data.
This is great for our needs because we can use this approach to create our own test data. The builder pattern enables us to create our test data with clear intent. This means that the pattern is solidly implemented using domain knowledge.
However, I think this approach can be valuable because it gives us the following benefits.
- A place to make things quick.
- This prevents data duplication.
- Provides a way to control every step of the construction process. This leads to code that provides clear intent of the data.
- Solid implementation can be done using domain specific language. It also makes clear the intent of the data. Name the builder methods as you want to provide as much information as possible to the developer / tester.
- Since the construction of the object is included in the code, it can be designed to handle foreign keys in the code which reduces complexity.
- Also, it means that collections are made when needed. Collections can be navigated by accessing the last item through the convention.
Builder pattern in action.
For this project, I used .NET Core 5.0, EFCore, and SQLite. Includes xUnit for unit testing, AgileMapper for mapping, and flow claims for claims.
Our demo project is a new travel planner and is available on Gut Hub. It is based on a model of tourist tours that include sightseeing tours (such as Paris) and sightseeing tours (such as the Eiffel Tower).
The only part of the business logic is to change the layout of the place. This logic is code under test, relies on a database, and includes one
So now for the data. As mentioned earlier, we will mimic the database using a Memory MySQL Elite database as it will work well with our service. This will be incorporated using an xUnit fixture class so that all tests in the class can share the same data. Finally, we have a custom implementation of the builder pattern that is used to create test data.
What does a test builder look like?
ITourist The interface includes ways to create our tourists. You can see that the builder enables complex object models using step-by-step method names that match the domain. Finally ,
BuildTourist The method will return our item.
Now for the implementation sample. Initially, we make our own high-end product, a
Tourist Which is kept in a private field.
Next, we add one.
Excursion To our tourist
All other steps are the same as above for building the object. Finally ,
BuildTourist Procedures have been implemented that return us.
The test project includes an example of loading data using a builder pattern. Item order and indentation are important for reading ability, but not necessarily.
In contrast, it includes an example of loading the same test data directly through object creation.
Comparing these two different implementations, it is easy to see that using the builder, the test data is more readable and easier to maintain.
Now to the unit test. Of
TestsWithFixtureAndBuilder Uses class fixtures where our test data is generated. There is only one test in the test class to check that
SwapPlaceVisits The procedure works as expected.
Remember to see the full example of the code in GitHub.
More ideas for test data builder.
Since this is custom code, there is a lot of flexibility available and you can extend the basic test data builder:
- Include verification rules. It can be helpful to make sure that business logic barriers are correct before running a test and to make sure that your test data is correctly generated.
- Set key keys automatically. Depending on how the tests are configured in your particular case, the builder may have the opportunity to configure the basic keys.
- Create a model with default values and only update specific values. This can be done using lambda functions.
- Add simple English specifications to the builder and override the ToString () method. Text information can be added online. This unit can also be output during test execution to help with debug trouble testing.
- Include factory methods for generating general test data.
As we have seen, there are many ways to create test data. However, when data begins to become complex, it becomes difficult to understand and maintain, leading to complications in our unit tests. The builder pattern has existed for a long time and has been useful in contextualizing test data and making it easier to understand and maintain test data.
Hopefully, this article and code example will provide some useful tips for building your test data. Aside from this particular code example, Takeway Points is about finding the approach that works best for you and the rest of your development and testing team. So when creating test data, aim for the following:
- Test data is easy to understand.
- The data relationship is easy to understand.
- It’s easy to change or add data.
- The approach ensures that developers adhere to the DRY principle.