When do I write unit tests

Unit Tests Tutorial - Test Automation at Different Levels - Part 2

Tutorial goal:

In the first part of the series, the basic test automation approaches and test stages as well as our example test scenario were presented.

In the second part of the series, the basics of test automation at the unit test level are explained in more detail using this test scenario.

Among other things, the following topics are covered:

  • What are the unit tests and their USPs?
  • What should be tested using unit tests?
  • The basic components of a unit test?
  • Which unit tests frameworks are there?
  • Establishment of a development environment
  • Creation of a unit test project
  • Creation of the unit tests for the example scenario
  • Maintenance and lifecycle of the unit tests

1. What are unit tests?

Unit tests check on a technical level whether the code fragment written by the software developer is working properly. They are designed as fine-grained, atomic, isolated tests that test the smallest units of the implemented code (at the level of individual functions and classes). When implementing the unit tests, try to mock as much as possible all dependencies on other classes, modules and data so that such tests can run as independently, parallel and efficiently as possible in a unit test runtime environment (e.g. in IDE or on a build server).

Unit tests are mainly implemented as white box tests that cover both the positive and negative interaction paths and states in the respective code units.

Note:

The technical means for carrying out the unit tests are very often used to “package” the tests at higher levels, including surface tests, as unit tests so that they can be conveniently transferred in the same way as the “real” unit tests an IDE, Build Server or Continuous Integration environment can be started. Such tests are then actually (from a technical point of view) mapped as unit tests because they use the unit tests as physical "containers" (see also Structure of unit tests). Methodologically, however, they should never be confused with the real unit tests, because at least 2 characteristic features of unit tests are not fulfilled:

  • Test focus on small atomic technical units of a code fragment instead of entire technical functionalities (which are usually composed of several atomic units)
  • Direct call of the code elements (classes, functions) instead of interaction with built and executable application (e.g. web server)

Objectives of the unit tests:

The overriding goal is to use the unit tests to test all important components of the implementation with a high level of test coverage. Depending on the business requirements and criticality of the software product, the requirements for the minimum meaningful code coverage of the unit tests vary.

For normal, non-critical software products, line code coverage of at least 60% should be aimed for. In the environment of critical systems such as medical technology, significantly higher unit test code coverage values ​​with branch coverage of over 85% are usually required.

Tip: The purely numerical (quantitative) code coverage must never be used as a feature for attesting good software quality (even with values ​​of over 90%). Unfortunately, the numerical code coverage does not provide any basis for assessing the quality of the unit tests carried out. It is also important to ensure that unit tests contain the relevant tests (assertions) (see below) and cover the required data constellation / equivalence classes. Conversely, test code coverage should therefore be perceived as an indicator of inadequate quality assurance at the unit test level, from which action actions can then be derived.

Advantages of the unit tests:

The great advantage The unit testing is that you can do them in a very early software development phase write and use immediately during development immediate review who can use pieces of code that have just been programmed.

With this, unit tests can also unfinished and non-executable Test software components. The only requirement for most programming languages ​​is that the software module to be tested can be compiled.

Due to the low (or in the best case even no) dependencies on other modules and data, the unit tests don't just run lightning fastbut usually very stablewith an extremely low flaky rate. This is because, due to the high level of isolation, there are hardly any external events that can influence the behavior or stability (such as changes to the database in the database, non-availability of a web service, etc.)

2. What should be tested using unit tests?

Due to the high isolation and fine granularity of the unit tests, they are predestined for testing the technical implementation of individual functions that encapsulate important technical algorithms, for example. With the help of the unit tests it is not only possible for everyone technicalTo test paths within a function, but especially the technicalVerify borderline cases.

For example, you can check whether the function reacts correctly to invalid values ​​outside the value range and unexpected internal states. Just as important is the verification of whether internal operating system errors, such as network socket closed, are being handled. The last example in particular can be tested very purposefully and comparatively with little effort with unit tests, while the same tests at higher levels may be significantly more complex or not possible at all because the error events may be intercepted or masked by higher layers.

In addition, with unit tests it is even possible to explicitly test the internal, non-open methods of a class or an object, provided that these unit tests are developed as white box tests and reference the code to be tested. With the automated tests at higher levels, only an indirect test of such methods is possible.

Despite the charm of being able to test many internal functionalities by means of unit tests, it is important when selecting the code areas to be tested to always prefer the most important parts (e.g. core functionalities from the business perspective with high frequency of calls) so that the unit tests always have high relevance. This is important because as code coverage increases, the effort required to further increase code coverage tends to increase exponentially and you want to reduce uneconomical costs.

 

3. The basic components of a unit test?

Each unit test consists of the following components:

  1. Input: Provision of the required input data
  2. Interaction: Call of the functional unit to be tested
  3. Assertion: Verification of the results after calling the function with the expected results

Pro tip:

That sounds banal, but the third part in particular is either completely forgotten / left out or only insufficiently covered in practice. The verification of the results is the central component of the unit tests. Without such verification, the unit tests are worthless in most cases. After all, we don't just want to check whether the function to be tested runs without exceptions, but actually fulfills its technical objective. It is important for thatas many assertions as possible (but as little as necessary) to be implemented in the unit tests. Please do not save at this point - you will definitely be grateful for that later!

In addition, there is the option of providing the required input data for the related set of unit tests as part of a "tear-up" phase or rolling back the changes made using the "tear-down" phase.

4. Which unit tests frameworks are there?

Since unit tests are mostly coded as white box tests and directly reference / integrate the software modules to be tested, they are typically implemented in the same programming language and the same programming environment.

The following list summarizes the most popular unit test frameworks in the context of some common programming languages:

programming languageUnit test frameworks
JavaJUnit, TestNG, Spock
C #MS Unit, NUnit, xUnit
Java ScriptJSUnit, Jasmine, Karma
pythonPytest

Partner offer: practical training

Do you work in a test environment and would like to receive further training in C # / UnitTest / Test Driven Development?

We conduct practical training courses in this area at regular intervals!
Training courses taking place soon:

5. Establishing a development environment

For the implementation of our unit tests, we use the .NET environment and the MS unit test framework below. You can also try out the example with other languages ​​such as Java using Eclipse.

To set it up, we only need the current development environment Microsoft Visual Studio, which we download and install in the free community version.

Visual Studio 2017 - Community Editon - Windows

After the installation you can finally select the required work modules.

For this tutorial, please select the following modules:

  • ".NET desktop development"
  • Cross-platform .NET Core development
  • ASP.NET and web development

6. Creation of a unit test project

Best practice is to create the unit tests in the same project as productive code. In this way, the unit tests can support the developers even more effectively during implementation, are always at hand, so to speak, and can be refactored and adapted at the same time as the productive code. In addition, they are then subject to the same versioning criteria for the source code.

For this reason, we start our exercise first with the creation of a joint project collection (solution) in which we first create the C # project for the implementation of our "multiplier" application and then add an MSUnit test project to test the application.

Create a new C # solution and class library:

Open Visual Studio, click on "New" -> "Project" in the file menu.

A dialog then appears in which you select the type of project that is automatically created when the solution is created and is used for the implementation of our sample application. Select “Visual C #” -> “.Net Core” -> “Class Library (.Net Core)”.

Enter "Multiplier" as the solution name.

Create a new unit test project:

In the next step we add a new unit test project. To do this, click on “Add” -> “New Project” in the file menu.

A dialog then opens again in which we select the type of unit test project for our project this time.

Please select “Visual C #” -> “.Net Core” -> “MSTest Test Project (.Net Core)” and name this project “UnitTests”.

After completing these actions, we have a solution with 2 projects, the structure of which is displayed in the Solution Explorer.

  • The “Multiplier” project contains a predefined class “Class1.cs” in which our multiplication can be implemented.
  • The “UnitTests” project contains a predefined class “UnitTest1.cs” in which we will write our UnitTests.

7. Creation of the unit tests for the example scenario

Preparation: Implement multiplication out of the box

Next, we take on the development-side part of the task and implement the requirement defined in the user story.

At the beginning we rename the predefined file "Class1.cs" after "Multiplikator.cs" in order to improve the code readability.

  • Right click on "Class1.cs" -> Rename -> enter "Multiplier".
  • Visual Studio automatically suggests renaming the class name and all possible uses. Confirm this with "Yes"

Double-click on the "Multiplikator.cs" file to open the class in VS Editor, in which we insert the following implementation of multiplication as a class method

public int Multiply (int operand1, int operand2) {return operand1 * operand2; }

Create unit tests

Now we can write the suggested test cases for our example scenario that test the newly implemented multiplication.

preparation

1. First, we rename our predefined unit test class from “UnitTest1.cs” to “MultiplikatorTests.cs” and confirm the request with “Yes”.

2. Double-click on the “MultiplikatorTests.cs” file to open the class in VS Editor.

A functional template for a unit test is already in the class.

Let's go through the most important parts briefly:

  • The file automatically includes Microsoft.VisualStudio.TestTools.UnitTesting namespace, in which special functions for unit tests are available
  • The class was marked with the special attribute [TestClass].
  • The empty method TestMethod1 was marked with the special attribute [TestMethod]

Both attributes are of central importance, because it is precisely these attributes that characterize the Unit Test class as a collection (= test scenario) of unit test methods (= test cases) that are automatically recognized by the Visual Studio Compiler and offered for test execution (more on this later).

Note: Other UnitTest Frameworks may have syntactically different markings, but the principle remains the same everywhere.

Insert unit test

3. In the next step we rename the method “TestMethod1” to “TestPositiveMult” and add the following C # code to this unit test method, which implements our first unit test case and contains all the components of a unit test presented. Our "TestPositiveMult" unit test looks something like this:

[TestMethod] public void TestPositiveMult () {// 1. Input int operand1 = 2; int opernad2 = 3; // Expected int expResult = 6; // 2. Call SuT Multiplikator.Multiplikator mult = new Multiplikator.Multiplikator (); int result = mult.Multiply (operand1, opernad2); // 3. Validation Assert.AreEqual (expResult, result); }

To validate the result, we use a special “Assert” class that is provided by the MS Unit Test Framework.
This class contains numerous special methods around the results for equality / inequality and much more. to be checked (see fig.)

Note: The same applies here: other unit tests frameworks can provide functions that differ syntactically, but the principle remains the same for all frameworks.

The assertion checks whether the current result matches the expected one and, in the event of an error, throws an exception, whereby the test case is automatically marked as a failed test case by the Unit Test Framework after execution.

Add code dependencies

4. Unfortunately, our implementation of the unit test is not yet fully functional. This is due to the fact that in the test case we call the Multiply method of our software under test, but have not yet linked this SuT module with our unit test project.

We'll do that right away by right-clicking on "Dependencies" and then on "Add reference ...".

In the selection dialog we activate the check box for the "Multiplier" project and confirm with OK.

This means that our UnitTest can directly access the implementation in the Multiplier project.

This is where the white-box character of the unit tests becomes clear. You reference the software-under-test module directly and interact with it by instantiating the objects of the SuT classes and calling their methods.

Run unit test

5. Our first unit test is now ready to run and we can start it immediately.

Right-click on the implementation of the unit test and select the option "Run Tests" from the context menu.

The results of the (successful) test execution appear within one second in the "Test Explorer" of Visual Studio (see fig.).

This window lists all test cases within the current project folder. From here you can run any tests again at any time.

Create further unit tests

As an exercise, copy the implementation of the first unit test and add 2 further unit tests below the first unit test to cover the 3 suggested test cases in our example scenario. You only have to change the (technical) names of the unit test methods and adapt the input parameters and the expected results.

After the file has been saved, the new unit tests are automatically recognized and offered for execution in Test Explorer.

Now let all 3 test cases run. The runtime specified as 8 ms illustrates the possible high speed of unit test execution as described above.

Fast feedback loop:

As a supplementary task, try to simulate a bug in the implementation of our demo application in the Multiply method, e.g. by replacing the multiplication sign with an addition. Repeat the unit tests immediately after the adjustment. How do the test results change? How long did the tests take to detect the error after the change was made?

Summary

Congratulations, you have just successfully implemented and executed your first unit tests!

The concepts and examples explained in this part offer you a basis for the initial introduction to the subject of "Unit Test Development". The best thing to do is to try it out using your own software components as an example! Start with simple methods first. As part of the practical examination of the unit tests, you will sooner or later have to deal with advanced topics: mocking, (unit) testability of complex methods, refactoring, dependency isolation, continuous test coverage measurement and integration into CI ...

In the next part of the series we will deal with the integration tests ...

Series parts:

Editor on Testautomtisierung.org
Managing director, training manager at SimplyTest GmbH, Nuremberg
www.simplytest.de
Passionate software developer and test automation advocate with many years of professional experience as a software developer, test automation manager, team leader and project manager
/ 0 comments / by Alexander HeimlichKeywords:Basics, how to, test automation, tutorials, unit tests