Background
This post comes about as we take a fresh look at unit testing at work and the tools that we use. We have been building out some cool stuff, but very much moving fast (and errm, sometimes breaking things), and have planned a bit of gardening and housekeeping for the next few sprints and then embedding unit testing further into our processes.
As a bit of an aside, technology and the rate of change is relentless and even when you’ve mastered something that, at the time was the best practice, it will inevitably get surpassed by something else. For some 15+ years I’ve been using nUnit (and jUnit before that having moving across from Java). It is a solid testing framework and served me well, but I’ve been meaning to explore xUnit for some time, and have finally carved some time out to have a play.
This post is the first of a few. This will give an intro to unit testing and will consolidate thoughts on the approach that we’ll be trying out next over at cortex towers.
What is unit testing?
Unit testing is the process of testing individual units or components of software in isolation. It involves testing each unit of code to ensure that it works as intended, and that it integrates correctly with other units. It is usually done by developers using automated testing tools and focuses on testing the smallest testable parts of the codebase.
Are there other types of testing?
- Component Tests
Component testing is the process of testing a group of related units or components that perform a specific function within the software. It involves testing the interactions between components and ensuring that they work together correctly. Component testing is usually done by developers or testing teams using automated testing tools, and it can be done in isolation from the rest of the system.
- End to End Tests
End-to-end testing is the process of testing the entire system from start to finish, including all the components and their interactions. It involves testing the software as a whole to ensure that it meets the requirements and works as intended. End-to-end testing is usually done by testing teams, and it can involve both manual and automated testing. It is usually the final step in the testing process before the software is released.
Why bother?
Unit testing is an indispensable part of software development and provide some direct and indirect benefits…
- They validate individual components of software, ensuring each behaves correctly. By isolating parts, defects are easier to identify and rectify.
- Unit tests document behavior, serving as a design guide and usage manual.
- They tend to improve the structure of code (decoupling), making it more maintainable and less prone to bugs. It simplifies integration by ensuring units work separately before combining.
- They safeguard against regression, alerting developers when a recent change breaks existing functionality.
Let’s look at a simple xUnit test
We’ll use the a string extension method Left()
as our test subject.
This method extracts the first maxChars
characters from the input string. If the string is shorter than maxChars
characters, it returns the entire string. maxChars
is optional and by default it returns the first 10 characters.
|
|
So, for example, var leftie = "The cat sat on the mat.".Left(15);
will return the value "The cat sat on "
.
And a simple test for the case when called with an empty string is shown below.
|
|
Points to note
Unit Test Naming Convention
We will use the MethodName_ExpectedBehavior_StateUnderTest
convention as the naming strategy as it provides clear, descriptive names for test methods.
- MethodName: This part represents the name of the method that you’re testing.
- ExpectedBehavior: This part describes what behavior is expected when the conditions of the test are met. It should be able to answer what outcome you expect from the method under certain conditions.
- StateUnderTest: This part describes the state under which the method is tested. It generally includes the conditions under which the test takes place.
In our example, Left_ShouldReturnEmptyString_WhenSourceStringIsEmpty
above.
In this example:
Left
is the MethodNameShouldReturnEmptyString
is the ExpectedBehaviorWhenSourceStringIsEmpty
is the StateUnderTest
The benefit of this convention is that the test’s purpose is immediately clear just from reading its name, making it easier to understand and maintain.
FluentAssertions
We will use the FluentAssertions
nuget package. FluentAssertions is a .NET library that offers a more readable, fluent interface for writing assertions in your tests. Its powerful, syntax simplifies complex assertions on objects and data types, making tests easier to write, read, and maintain.
FluentAssertions are covered off in more detail here
The Trip-A approach
The AAA (Arrange-Act-Assert) approach is a common unit testing pattern that provides a clear structure for organizing test code. It involves three steps:
- Arrange: In this step, you set up the necessary preconditions for the test, such as creating objects, initializing variables, and defining inputs.
- Act: This step involves performing the action or operation that you want to test, such as calling a method or executing a function.
- Assert: In this step, you verify that the result of the action is as expected by comparing it to the expected output or behavior. By following the AAA approach, you can create clear and readable tests that are easy to understand and maintain. It also helps you to identify the cause of any failures or errors that may occur during testing.
A deeper dive into xUnit
Lifecycle of an xUnit unit test
xUnit ensures that each test method runs in isolation, and any dependencies or shared resources are properly initialized and cleaned up. It does so by instantiating the test class afresh for each test and running the following in order…
-
Test Class Initialisation: Before running any tests in a test class, the test framework will create an instance of the test class and call its constructor. If the class has a constructor that requires parameters, the framework will attempt to resolve those dependencies through a dependency injection mechanism. This is used in place of the
[setup]
attribute that nunit uses. -
Test Method Execution: Once the test method setup is complete, the test method itself is executed.
-
Dispose: You can also use the IDisposable interface to release any unmanaged resources used by your tests, such as database connections or file streams.
You need to implement the IDisposable interface and include a Dispose method that releases any unmanaged resources. The Dispose method will be called by xUnit after all test methods have been executed, and it’s a good place to perform any cleanup.
|
|
In xunit, there are no dedicated setup and teardown methods like in nunit’s [setup]
and [teardown]
attributes or mstest’s [testinitialize]
and [testcleanup]
attributes. Instead, xunit encourages the use of constructor and idisposable for setup and teardown operations.
Xunit’s approach of using constructor and idisposable for setup and teardown operations aligns with its philosophy of simplicity, test isolation, flexibility, and interoperability, and provides a clean and efficient way to organize and execute unit tests.
Facts and Theories
In xUnit, Fact
and Theory
are two types of unit tests that you can use.
-
Fact: A Fact in xUnit signifies a test method that should always succeed. It does not take any parameters, and no argument data can be passed to it. Fact is used when we have some deterministic piece of code that can have only one outcome — success.
-
Theory: A Theory in xUnit signifies a test method that should succeed for certain input data. Theory is used when we have a method that should work for a range of data and can take parameters, which are defined using InlineData, ClassData, or MemberData attributes. It is basically a way of doing data-driven testing.
Let’s look at an example of a Theory test type.
We’ll use a DateTime extension method GetDaySuffix() as our test subject. This extension method returns the suffix for the day number of the month.
|
|
So, for example, var suffix = DateTime.Parse("2021-01-01").GetDaySuffix();
will return the value "st"
.
|
|
System Under Test
“System Under Test” is normally abbreviated to sut
. It’s a naming convention used to represent the main object or component that is being tested in a particular test case or suite.
Using _sut
makes it clear what object is being tested and can make your tests more readable and maintainable.
The examples we have seen so far have been simple extension methods. Our tests will more likely be testing classes, be they utility, helper or service classes. You would typically declare you “System Under Test” at the top of your test suite (class).
Assume we have a Customer class with a FullName method that combines FirstName and LastName properties.
|
|
Then, a unit test class using xUnit.net might look like this:
|
|
You would likely have many test methods testing the behaviour of the sut.
Wrapping up
This has introduced some of the xUnit basics, along with FluentAssertions. A key part of UnitTesting is mocking. We’ll look at this next.
Acknowledgements…
I usually use Udemy or youTube to rattle through a course, but decided to give Nick Chapsas’s course a try. His youTube material is excellent and are well worth subscribing to, but highly recommend this course. Nick’s videos are always excellent. He cuts out any unnecessary fluff and just tells you what you need to know.