.NET Core 101 Unit tests. To create functional, reliable… | by Justin Muench | Mar, 2022

To create functional, reliable applications

Photo by Scott Blake on Unsplash

The basic idea of ​​unit tests was introduced in 1970 by Kent Back in Smalltalk. Unit tests are among the best tools to increase software quality and are usually written in a particular test framework.

Simply put, a unit test is a piece of code that calls another piece of code (unit) and checks if the output matches the desired outcome.

In the most original definition, a unit was a method or a function. Today one also speaks of a unit of work, which is:

“[…] the sum of actions that take place between the invocation of a public method in the system and a single noticeable result by a test of that system.”

(Chapter 1. “The basics of Unit Testing — The Art of Unit Testing” by Roy Osherove).

This means that a single software component or method is tested. Only code under the developer’s complete control should be tested. However, according to Roy Osherove, author of “The Art of Unit Testing: With Examples in C#,” one should not try to minimize the size of a unit of work under test. If you try to reduce the size of a unit of work, you are faking things that are not a result for the user of a public API.

Besides unit tests, there are many other tests. In the following, I would like to discuss the following standard tests: integration, functional, and acceptance tests.

Integration Tests

Unlike unit tests, integration tests are not self-sufficient. They usually have additional dependencies outside the system under tests, such as databases or a file system. The cooperation of two or more software components is tested. Integration tests can also involve infrastructure issues. Integration tests use actual dependencies, while unit tests isolate the unit of work from its dependencies.

Functional Tests

Functional tests are to be considered high-level testing. The functionality from the user requirement is to be tested; Therefore, the system is also considered a black box. A part of a functionality of a whole system is tested.

Acceptance Tests

Acceptance testing is also considered high-level testing. It is based on user and business specifications. It is tested whether the software has the intended business value. Finally, they check whether the business and contractual requirements are met.

Source: Introducing Test Driven Development in C# By Nikola Živković (Published by Packt Publishing)

The pyramid above answers which tests should ideally be written and how many. As you can see, unit tests play the most important role. This is because they are easy and fast to write, and the execution is the fastest.

The following types of tests are more time-consuming but still not negligible.

Automated Testing

Once we have written tests to check our code, we can also run those tests repeatedly in an automated fashion. The problem with manual testing is that it is not nearly as efficient as unit testing.

For example, to test the logic of a class (for a webpage), the application must be launched, it must potentially be logged in or registered, it must be navigated to the page, and then implemented changes must be tested and validated. This process is a very time-consuming one.

Automated tests can now be used to make the whole process more efficient.

Thereby automated tests can be any of the test types shown above.

  • You write them once but can run them as often as you wish.
  • Once written, tests can run at any time, whether triggered manually or automated.
  • Automated testing helps us detect bugs before we deploy.
  • The tests are very reliable and efficient.
  • When publishing our code, automated tests give us confidence.
  • And one of the most important reasons is that it allows us to refactor and make sure that the functionality is still there afterward.

When writing automated tests, a typical order has become accepted: arrange, act, assert. This structure is also observed when writing unit tests.

The first phase is the initial setup, like creating object instances and test data. The execution of the code follows this in the act phase. In the last phase, we want to check whether the result corresponds to our expected result.

There are a lot of test frameworks for writing tests. Below, I have listed a few and the respective pages in the Microsoft documentation for more information.

xUnit

NUnit

MSTest

There is an easy answer to the question. Every software developer should write tests. Testing the software for high quality and accuracy must be as much a part of the development process as, for example, consulting on technical requirements.

If a car or an airplane is developed, also a form of product, it is also expected to be tested. Developers and the requesters of software should also make the same demands on the software.

Once you have acquired the knowledge about well-structured, maintainable, and solid tests with a test framework, at some point, the question arises: When do we write the tests? Many developers write their tests after the software is written, but a growing number nowadays write the tests before the code is produced. The approach of writing tests before the production code is called test-first or test-driven development.

Let’s start by creating a new class library project.

For this example we want to create now, we make a calculator which adds two numbers. For this, we rename the existing class to calculator and write a method that adds two numbers and returns the result.

So, after renaming the class and writing an addition function, we now need to add our test project to the solution. In this example, we will use Microsoft’s test framework MSTest provided.

We now want to rename the UnitTest1 class. We handle the naming as follows: Class to be tested plus “Should.” The same applies to the test method names. Name of the method under test plus Should do this or that.

In Arrange, we first initialize our critical test data and objects, as you can see.

We now execute the method in the Act phase and save the result.

We want to check the result and see if it matches our expected result in the last step.

To run this test now, we need to open the Test Explorer.

After running the test, you should see that it worked.

To make the test fail, you can change the expected result in the assert; then, the test will fail.

Attentive readers might want to remember that unit tests are not supposed to test dependencies such as databases, FileSystem, and others. Through a mock, we can imitate the behavior of a class or interface to isolate the code under test. We want to make sure that other code called does not cause our code under test to fail.

To demonstrate mocking, let’s add logging functionality to our calculator. Even if this is not entirely useful, the basic concept can be transferred.

First, we want to create an interface ILogCalc and a class LogCalc in our class library. Usually, the first thing we have in production applications is an interface implemented by a class. There are several reasons for this, but I won’t go into them now. But we must use the interface because it allows us to mock.

These are now to be initialized via the constructor within the calculator class.

As a rule, the structure is as shown above. We have a private field variable and initialize it via the constructor. This procedure is called Dependency Injection.

The nice thing about this setup is that we can now mock the interface in our test.

We should now get an error in our calculator test class with this setup because we can’t initialize the calculator without parameters anymore. But in our Calculator test, we don’t want dependencies on other classes and methods.

To use mocking, we first need to install a NuGet package. This is called Moq. Install this in our Test project.

What we can do then is create a mock of our interface. As shown below, we can then pass this to our calculator via the constructor.

Because our LogCalc log function has no return value, it is sufficient to pass the mock object.

However, if our log function, for whatever reason, has a return value such as True or False as to whether the logging worked, then we still need to adjust.

Let’s look at this for a moment. Here we change the signature of the log function in the interface and the class and return a boolean.

Now we can call a function called setup from our mock, through which we can mock the function and its return value.

In line 16, we use setup to mock the log method. We say here via It.IsAny<String>() that we accept any string value and always return true. If you now debug through the test, you should see that the log method returns true.

  • A 100% code coverage with unit tests is considered unrealistic and is mostly not aimed for. (Except perhaps for extreme programming)
  • Good code coverage is already achieved at 65–75%.
  • All paths should be tested if methods have multiple paths (even if this is not very nice due to clean code).
  • Tests force us to write clean code. Otherwise, it would not be testable.
  • Testing is essential to improve software quality and minimize errors.
  • Tests are an essential requirement for good refactoring.
  • There are different tests, eg, Unit Tests, Integration, Functional, and Acceptance Tests.
  • The basic structure of tests is arrange, act, and assert.
  • Mocking is used to free methods from their dependencies during unit testing.

Leave a Comment