Insights

How unit testing leads to better quality applications with fewer bugs

An overview of how unit testing helps to build better applications with fewer bugs.

in IT Strategy, Software development By Pedro Borges, Software Engineer

Here at Hexis, we believe in the importance of Unit Tests and how it translates to applications having better quality with fewer bugs. Even if you do not use a Test-Driven Development (TDD) methodology, unit testing should not be overlooked and forgotten.

We know that some tests are cumbersome and extensive to write, that's why we want to help, by introducing two NuGets packages/tools that will make some steps of Unit testing easier.

What is a Unit Test?

Let's define what a Unit Test is and some concepts behind it.

A Unit Test is a software testing method where individual units of software are tested, determining if they are developed correctly.
The main keyword is unit because, in these type of tests, we should focus on small pieces of code/logic. All the unit tests should only test a specific point in the code and not flows of execution (which is named Integration Testing).

For example, imagine that we have a method called IsWordUnit, that receives a string and checks if it is equal to the word "unit". This method will produce, at least two units tests (it could be more): one to check if it works when receiving the word "unit" and another with a different word.

The following test example refers to the second case, where we pass an input different than "unit":

(This Unit Test example was done using C# and the XUnit framework)

  • [Fact]
    public void IsUnitWord_InputIsDifferentThanUnit_ReturnsFalse()
    {
         //Arrange
         var input = "notUnit";

         var fooBar = new FooBar();

         //Act
         var result = fooBar.IsUnitWord(input);

         //Assert
         Assert.False(result);
    }

As shown in the example above, the test is divided into different parts. Usually, a Unit Test has three different phases; the 3 A's: Arrange, Act, and Assert. These phases are guidelines when creating Unit Tests, helping understanding where everything is in the code, the different phases that a test should have, and creates a standard format for every test, creating a more homogeneous code.

The Arrange phase is the first one, where we need to prepare our test. Instantiate all the variables and classes that we are going to need for our test. It could be specific parameters, class instances, mocking classes, and even define the expected result for the test (only instantiate the variables, we do not compare anything yet).

The second step is the Act phase. Here we are going to call the logic we want to test. We will use the class that was instantiated in the Arrange phase and call the method where the logic we want to test is hosted. If it returns something, that needs to be saved to use in the next phase.

The last phase is the Assert phase, translates into the last thing to do in the test is to see if it had the expected result. In this phase, we will need to compare the results from the Act phase with the expected results (that can be defined in the Arrange phase). If the logic is correct, the test will pass!

Remember, we should create Unit Tests where we expect the logic to fail! An exception can be an expected result and is a valid test. This way, we will have a much more precise view of the implemented logic.

AutoFixture

One of the least enticing things about the Assert phase in unit tests is to populate all the unnecessary variables that a method requires to function. Often, a Data Transfer Object (DTO) will have several variables that do not affect the test, but to compile the code we need to define them, this where AutoFixture enters!

As the AutoFixture creator, Mark Seemann describes it this way: "AutoFixture is designed to make Test-Driven Development more productive, and unit tests more refactoring-safe. It does so by removing the need for hand-coding anonymous variables as part of a test's Fixture Setup phase". AutoFixture creates values of any type without explicitly instantiate them. This way, we do not need to explicitly write and come up with a value for the test.

In the example above for IsWordUnit test, where we evaluate if an input word is different from "unit", we can write something like "notUnit" and the test will work, or we let AutoFixture automatically generate a word for the test. This feature becomes extremely useful because:

  • 1. We do not need to come up with a random string. We know that in this case is just one string, but in more complex applications, some DTO's have several variables, and then we need to come up with even more values and for different types!
  • 2. Each time we run the test, a different value will be generated. This particularity is useful (sometimes of course) since the value of the variables should be independent of the test. Besides "unit" any value should make the test pass. In the case of strings, AutoFixture uses a GUID as a value, so we know it will never be "unit".
    In other tests, where variables do not affect the test, having different values every time we run the test, ensures the integrity of the code and makes for a better test.

The next piece of code shows how the previous test can be done using AutoFixture:

  • [Fact]
    public void IsUnitWord_InputIsDifferentThanUnit_ReturnsFalse_AutoFixture()
    {
         //Arrange
         var fixture = new Fixture();
         var input = fixture.Create<string>();

         var fooBar = new FooBar();

         //Act
         var result = fooBar.IsUnitWord(input);

         //Assert
         Assert.True(result);
    }

Only the Arrange phase was changed, where we switched the "var input = "notUnit";" by "var input = fixture.Create<string>();”. The create method of AutoFixture will create a random value for the type that was specified, in this case, a string.

If it were a complex type, it would look something like "var input = fixture.Create<GiantDto>();” where GiantDto is a complex DTO with several variables, and AutoFixture would fill them with random values!

Furthermore, AutoFixture as a lot more features! For example, what happens if we need to have a specific value for a variable inside a DTO, but everything else can be random? AutoFixture already has that covered! AutoFixture has a method called "Build" where we can specify behaviour for every variable and/or types inside a specific DTO!

  • var expectedResult = fixture
         .Build<GiantDto>()
         .With(t => t.OneOfTheVariables, “specificString”)
         .Create();

And for us, this is one of the most powerful features of AutoFixture, where we can specify the behaviour that we want! We can specify by variable level, type level, or even class level!

AutoFixture offers even more features and extensions, and everything can be found in the official GitHub page of AutoFixture: https://github.com/AutoFixture/AutoFixture.

FluentAssertions

Jumping from the Arrange phase to the Assert phase, another tool that we want to present is the FluentAssertions.

FluentAssertions is a tool that helps in the Assert phase by enabling a simple, intuitive syntax for easy comprehension of the assertions and expected outcome of the unit test.

FluentAssertions almost makes the assertions look like they were written in plain English! A test example requiring a result string to start with a 'z' and ending with 'a' with a total length of 5, would look like:

  • var result = "zebra";
    result.Should().StartWith("z").And.EndWith("a").And.HaveLength(5);

With one line, we have three different assertions, and with clear and intuitive syntax.

One of the problems with the standard assert framework is that complex types, like DTO's classes, can only be compared if we compare the content of the class, variable by variable, or create an Equal function for the class. This limitation happens because of the Assert.Equal behaviour. It will only compare if the two objects are the same and not if they have the same content!

FluentAssertions solves this issue by having a keyword, BeEquivalentTo, that compares the content and structure of two objects:

  • resultDto.Should().BeEquivalentTo(expectedDto);

This way translates to an easier and much more straightforward comparison of DTO's.

This logic was also applied to other types, like collections. Instead of using a for to iterate and then compare everything, we use Should().Equal(...) and it will compare two collections. Even better, we can specify the assertions in multiple ways, like ignoring if the values are in the same position or even defining expressions to compare the expected results.

FluentAssertions provides numerous ways to define the whole Assertion phase for each different variable type, and some several different keywords and methods will give us new ways to assert results.

Everything about FluentAssertions can be found on the official website: https://fluentassertions.com.

Conclusion

Unit Tests are important for all types of developments, providing a way to deploy bug-free software and detect problems before they reach production environments.
However, writing a good and comprehensive Unit Test can be challenging, and tools as AutoFixture and FluentAssetion can support and facilitate this process.

AutoFixture helps by removing the need for hand-coding unknown variables during the Arrange phase. It automatically fills the content for any variable or class that we want, while providing a degree of control of how the variables should be filled, by allowing us to define behaviours.

FluentAssertions provides an English like syntax to define the expected outcome of a Unit Test and a variety of options when comparing results. Using FluentAssertions will make Unit Test more readable, easier to understand, and simpler to write.

Get in touch