Unit testing for dummies

  • Tutorial
Even if you never in your life thought you were doing testing, you do it. You collect your application, press the button and check whether the result matches your expectations. Quite often in the application you can find forms with the “Test it” button or classes called TestController or MyServiceTestClient .



What you do is called integration testing . Modern applications are quite complex and contain many dependencies. Integration testing verifies that several system components are working together correctly.

It does its job, but is difficult to automate. Typically, tests require that all or almost the entire system be deployed and configured on the machine on which they are running. Suppose you are developing a web application with a UI and web services. The minimum equipment that you need: a browser, a web server, properly configured web services and a database. In practice, it is still more difficult. Deploy all this on the build server and all the development machines?

We need to go deeper



First, let's go down to the previous level and make sure that our components work correctly separately.

We turn to wikipedia:
Unit testing or unit testing (English unit testing.) - the process of programming, which allows to check on the correctness of the individual modules of the program source code.

The idea is to write tests for each non-trivial function or method. This allows you to quickly check whether the next change in the code has led to regression, that is, to the appearance of errors in the already tested places of the program, and also facilitates the detection and elimination of such errors.




Thus, unit testing is the first bastion in the fight against bugs. Behind it is still integration, acceptance and, finally, manual testing, including “free search”.

Do you need all this? From my point of view, the answer is "not always."

No need to write tests if


  • You make a simple business card site of 5 static html pages and with one form of sending letters. The customer is likely to calm down at this, he does not need anything more. There is no special logic here, it’s faster to just check everything “with your hands”
  • You are engaged in an advertising site / simple flash games or banners - complex layout / animation or a large amount of static. There is no logic, only representation
  • You are doing a project for the exhibition. The term is from two weeks to a month, your system is a combination of hardware and software, at the beginning of the project it is not completely known what exactly should happen at the end. Software will work 1-2 days at the exhibition
  • You always write code without errors, have the perfect memory and the gift of foresight. Your code is so cool that it changes itself, following the requirements of the client. Sometimes the code explains to the client that his claim - gov does not need to be implemented


In the first three cases, for objective reasons (tight deadlines, budgets, blurry goals, or very simple requirements), you won’t get any benefit from writing tests.

We consider the latter case separately. I know only one such person, and if you did not recognize yourself in the photo below, then I have bad news for you.



Any long-term project without proper test coverage is doomed to be rewritten from scratch sooner or later.



In my practice, I have met many times with projects older than a year. They fall into three categories:
  • Without test coverage. Typically, such systems are accompanied by spaghetti code and retired leading developers. No one at the company knows exactly how it all works. Yes, and what it ultimately needs to do, employees present very remotely.
  • With tests that no one runs or supports. There are tests in the system, but what they are testing, and what result is expected from them, is unknown. The situation is already better. There is some kind of architecture, there is an understanding of what is weak coupling. You can find some documents. Most likely, the company still has a chief system developer, who keeps in mind the features and intricacies of the code.
  • With a serious coating. All tests pass. If the tests in the project really run, then there are a lot of them. Much more than in systems from the previous group. And now each of them is atomic: one test checks only one thing. The test is a specification of a class method, a contract: what input parameters this method expects, and what other components of the system expect from it at the output. Such systems are much smaller. They have the current specification. A little text: usually a couple of pages describing the main features, server diagrams and getting started guide . In this case, the project does not depend on people. Developers can come and go. The system is reliably tested and itself talks about itself through tests.


Projects of the first type are a tough nut, it is the hardest to work with. Usually their refactoring at cost is equal to or greater than rewriting from scratch.

Why are there projects of the second type?

Colleagues from ScrumTrek claim that the dark side of the code and the lord Dart Autotestius are to blame . I am convinced that this is very close to the truth. Mindlessly writing tests not only does not help, but harms the project . If earlier you had one low-quality product, then writing tests, without understanding this topic, you will get two. And doubled time for maintenance and support.

In order for the dark side of the code not to prevail, you must adhere to the following basic rules .
Your tests should:

  • Be reliable
  • Do not depend on the environment on which they run
  • Easy to maintain
  • Easy to read and easy to understand (even a new developer needs to understand what exactly is being tested)
  • Comply with a single naming convention
  • Run regularly automatically

To achieve these points, patience and will are needed. But let's get it in order.

Choose a logical test location in your VCS

The only way. Your tests should be part of version control. Depending on the type of your decision, they can be organized in different ways. General recommendation: if the application is monolithic, put all the tests in the Tests folder; if you have many different components, store the tests in the folder of each component.

Choose a way to name projects with tests

One of the best practices: add to each project its own test project.
Do you have parts of the system.Core, .Bl and .Web? Add more.Core.Tests, .Bl.Tests and .Web.Tests.

This naming method has an additional side effect. You can use the * .Tests.dll pattern to run tests on a build server.

Use the same naming method for test classes.

Do you have a ProblemResolver class? Add ProblemResolverTests to the test project. Each testing class should test only one entity. Otherwise, you very quickly slide into a dull go into the second type of projects (with tests that no one runs).

Choose a “talking” method for naming methods of testing classes

TestLogin is not the best name for a method. What exactly is being tested? What are the input parameters? Can errors and exceptions occur?

In my opinion, the best way to name methods is: [Test Method] _ [Script] _ [Expected Behavior] .
Suppose we have a Calculator class, and it has a Sum method, which (hello Cap!) Should add two numbers.
In this case, our testing class will look like this:

сlass CalculatorTests
{
        public void Sum_2Plus5_7Returned()
        {
 	    // …
        }
}

Such a record is understandable without explanation. This is the specification for your code.

Choose a test framework that suits you

Regardless of the platform, you should not write bicycles. I saw many projects in which automatic tests (mostly not units, but acceptance tests) were launched from a console application. No need to do this, everything has already been done for you.

Pay a little more attention to the review of frameworks. For example, many .NET developers use MsTest only because it is included with the studio. I like NUnit a lot more. It does not create extra folders with test results and has support for parameterized testing. I can just as easily run my tests on NUnit using Resharper. Someone will like xUnit's elegance: a constructor instead of initialization attributes, implementing IDisposable as TearDown.

What to test and what not?

Some talk about the need to cover the code 100%, while others consider this an unnecessary waste of resources.
I like this approach: draw a piece of paper along the X and Y axis, where X is the algorithmic complexity and Y is the number of dependencies. Your code can be divided into 4 groups.


We first consider extreme cases: a simple code without dependencies and a complex code with a large number of dependencies.

  1. Simple code without dependencies. Most likely here everything is clear. You can not test it.
  2. Complex code with lots of dependencies. Hmm, if you have this code, it smells of God Object and strong connectivity. Most likely, refactoring will be nice. We will not cover this code with unit tests, because we will rewrite it, which means that the signatures of the methods will change and new classes will appear. So why write tests that you have to throw away? I want to make a reservation that for this kind of refactoring we still need testing, but it is better to use higher-level acceptance tests . We will consider this case separately.

What remains with us:
  1. Complex code without dependencies. These are some algorithms or business logic. Well, these are important parts of the system, we are testing them.
  2. Not very complicated code with dependencies. This code connects different components. Tests are important to clarify exactly how the interaction should occur. The reason for the loss of the Mars Climate Orbiter on September 23, 1999 was a human-software error: one unit of the project counted “in inches” and the other “in meters”, and this was clarified after the loss of the device. The result could be different if the teams tested the “seams” of the application.


Follow the same style of writing the body of the test

The AAA (arrange, act, assert) approach has proven itself perfectly . Let's go back to the example with the calculator:

class CalculatorTests
{
	public void Sum_2Plus5_7Returned()
	{
		// arrange
		var calc = new Calculator();
		// act
		var res = calc.Sum(2,5);
		// assert
		Assert.AreEqual(7, res);	
	}
}


This form of writing is much easier to read than

class CalculatorTests
{
	public void Sum_2Plus5_7Returned()
	{
		Assert.AreEqual(7, new Calculator().sum(2,5));	
	}
}


This means that this code is easier to maintain.

Test one thing at a time

Each test should check only one thing. If the process is too complicated (for example, buying in an online store), divide it into several parts and test them separately.
If you do not adhere to this rule, your tests will become unreadable, and soon it will be very difficult for you to support them.

Addiction Fighting

So far, we have tested the calculator. He has no dependencies at all. In modern business applications, the number of such classes, unfortunately, is small.
Consider this example.

public class AccountManagementController : BaseAdministrationController
{
	#region Vars
	private readonly IOrderManager _orderManager;
        private readonly IAccountData _accountData;
        private readonly IUserManager _userManager;
        private readonly FilterParam _disabledAccountsFilter;
        #endregion
        public AccountManagementController()
        {
            _oms = OrderManagerFactory.GetOrderManager();
            _accountData = _ orderManager.GetComponent();
            _userManager = UserManagerFactory.Get();
            _disabledAccountsFilter = new FilterParam("Enabled", Expression.Eq, true);
        }
}

The factory in this example takes data on a specific implementation of AccountData from the configuration file, which does not suit us at all. We do not want to support the zoo of * .config files. Moreover, actual implementations may be database dependent. If we continue in the same vein, we will stop testing only controller methods and begin to test other system components with them. As we recall, this is called integration testing .
Not to test all together, we podsunem fake implementation (fake) .
We rewrite our class like this:

public class AccountManagementController : BaseAdministrationController
{
        #region Vars
        private readonly IOrderManager _oms;
        private readonly IAccountData _accountData;
        private readonly IUserManager _userManager;
        private readonly FilterParam _disabledAccountsFilter;
        #endregion
        public AccountManagementController()
        {
            _oms = OrderManagerFactory.GetOrderManager();
            _accountData = _oms.GetComponent();
            _userManager = UserManagerFactory.Get();
            _disabledAccountsFilter = new FilterParam("Enabled", Expression.Eq, true);
        }
        /// 
        /// For testability
        /// 
        /// 
        /// 
        public AccountManagementController(
            IAccountData accountData,
            IUserManager userManager)
        {
            _accountData = accountData;
            _userManager = userManager;
            _disabledAccountsFilter = new FilterParam("Enabled", Expression.Eq, true);
        }
}


Now the controller has a new entry point, and we can pass other interface implementations there.

Fakes: stubs & mocks

We rewrote the class and now we can slip to the controller other implementations of dependencies that will not go into the database, watch configs, etc. In a word, they will do only what is required of them. We divide and rule. We must test these implementations separately in our own test classes. Now we are testing only the controller.

There are two types of fakes: stubs (stubs) and moki (mock).
Often these concepts are confused. The difference is that the stub does not check anything, but only simulates a given state. A mok is an object that has expectations. For example, that a given class method must be called a certain number of times. In other words, your test will never break due to a “stub”, but because of moka it can.
From a technical point of view, this means that using stubs in Assert we check the state of the tested class or the result of the executed method. When using moka, we check whether the expectations of moka correspond to the behavior of the tested class.

Headquarters



[Test]
public void LogIn_ExisingUser_HashReturned()
{
	// Arrange
	OrderProcessor = Mock.Of();
	OrderData = Mock.Of();
	LayoutManager = Mock.Of();
	NewsProvider = Mock.Of();
	Service = new IosService(
		UserManager,
		AccountData,
		OrderProcessor,
		OrderData,
		LayoutManager,
		NewsProvider);
	// Act
	var hash = Service.LogIn("ValidUser", "Password");
	// Assert
	Assert.That(!string.IsNullOrEmpty(hash));
}


Mok



[Test]
public void Create_AddAccountToSpecificUser_AccountCreatedAndAddedToUser()
{
    // Arrange
    var account = Mock.Of();
    // Act
    _controller.Create(1, account);
    // Assert
    _accountData.Verify(m => m.CreateAccount(It.IsAny()), Times.Exactly(1));
    _accountData.Verify(m => m.AddAccountToUser(It.IsAny(), It.IsAny()), Times.Once());
}


Health testing and behavior testing

Why is it important to understand the seemingly insignificant difference between mokas and stubs? Let's imagine that we need to test an automatic irrigation system. There are two ways to approach this task:

Condition testing

We start the cycle (12 hours). And after 12 hours we check whether the plants are watered well, whether there is enough water, what is the condition of the soil, etc.

Interaction testing

We install sensors that will detect when watering has begun and ended, and how much water has come from the system.
Stubs are used to test the state, and moki - interactions. It is better to use no more than one mok per test . Otherwise, with a high probability you will violate the principle of "testing only one thing." At the same time, in one test there can be as many stubs as you like or mok and stubs.

Insulation Frameworks

We could implement moki and stubs on our own, but there are several reasons why I do not recommend doing this:
  • Bicycles are already written before us
  • Many interfaces are not so easy to implement with half a kick
  • Our self-made fakes may contain errors
  • This is additional code that will have to be supported.


In the example above, I used the Moq framework to create mobs and stubs. The Rhino Mocks framework is quite common . Both frameworks are free. In my opinion, they are almost equivalent, but Moq is subjectively more convenient.

There are also two commercial frameworks on the market: TypeMock Isolator and Microsoft Moles . In my opinion, they have excessive capabilities to replace non-virtual and static methods. Although this may be useful when working with legacy code, I will describe below why I still don’t advise doing such things.

Showcases of the listed isolation frameworks can be found here . And information on the technical aspects of working with them is easy to find on Habré.

Architecture under test

Let's go back to the controller example.

public AccountManagementController(
    IAccountData accountData,
    IUserManager userManager)
{
    _accountData = accountData;
    _userManager = userManager;
    _disabledAccountsFilter = new FilterParam("Enabled", Expression.Eq, true);
}

Here we got off with a little blood. Unfortunately, not everything is always so simple. Let's look at the main cases of how we can implement dependencies:

Constructor injection

We add an additional constructor or replace the current one (it depends on how you create objects in your application, whether you use an IOC container). We used this approach in the example above.

Factory Injection

Setter can additionally be "hidden" from the main application if you select the IUserManagerFactory interface and work in production code using the interface link.

public class UserManagerFactory
{
    private IUserManager _instance;
    /// 
    /// Get UserManager instance
    /// 
    /// IUserManager with configuration from the configuration file
    public IUserManager Get()
    {
        return _instance ?? Get(UserConfigurationSection.GetSection());
    }
    private IUserManager Get(UserConfigurationSection config)
    {
        return _instance ?? (_instance = Create(config));
    }
    /// 
    /// For testing purposes only!
    /// 
    /// 
    public void Set(IUserManager userManager)
    {
        _instance = userManager;
    }
}

Factory spoofing

You can replace the entire factory. This will require the allocation of an interface or the creation of a virtual function, the creation of objects. After that, you can redefine the factory methods so that they return your fakes.

Override the local factory method

If dependencies are instantiated directly in the code explicitly, then the easiest way is to select the factory protected CreateObjectName () method and redefine it in the derived class. After that, test the derived class, not your originally tested class.
For example, we decided to write an extensible calculator (with complex actions) and began to select a new layer of abstraction.

public class Calculator
{
    public double Multipy(double a, double b)
    {
        var multiplier = new Multiplier();
        return multiplier.Execute(a, b);
    }
}
public interface IArithmetic
{
    double Execute(double a, double b);
}
public class Multiplier : IArithmetic
{
    public double Execute(double a, double b)
    {
        return a * b;
    }
}

We do not want to test the Multiplier class , there will be a separate test for it. We rewrite the code like this:

public class Calculator
{
    public double Multipy(double a, double b)
    {
        var multiplier = CreateMultiplier();
        return multiplier.Execute(a, b);
    }
    protected virtual IArithmetic CreateMultiplier()
    {
        var multiplier = new Multiplier();
        return multiplier;
    }
}
public class CalculatorUnderTest : Calculator
{
    protected override IArithmetic CreateMultiplier()
    {
        return new FakeMultiplier();
    }
}
public class FakeMultiplier : IArithmetic
{
    public double Execute(double a, double b)
    {
        return 5;
    }
}

The code is intentionally simplified to focus on the illustration of the method. In real life, instead of a calculator, there will most likely be DataProviders, UserManagers, and other entities with much more complex logic.

Tested VS OOP Architecture

Many developers begin to complain, saying "this is your design under test" violates encapsulation, opens too much. I think there are only two reasons when this may bother you:

Serious Security Requirements
This means that you have serious cryptography, the binaries are packed, and everything is hung with certificates.
Even so, you are likely to find a compromise solution. For example, in .NET, you can use the internal methods and the [InternalsVisibleTo] attribute to give access to the tested methods from your test builds.

Performance
There are a number of tasks when architecture has to be sacrificed for the sake of performance, and for some it becomes an occasion to refuse testing. In my practice, docking a server / upgrading hardware has always been cheaper than writing untestable code. If you have a critical site, it is probably worth rewriting it at a lower level. Is your application in C #? Perhaps it makes sense to build one unmanaged assembly in C ++.

Here are some guidelines to help you write testable code:
  • Think with interfaces, not with classes, then you can always easily replace real implementations with fakes in test code
  • Avoid direct instantiation of objects inside methods with logic. Use factories or dependency injection . In this case, using an IOC container in a project can greatly simplify your work.
  • Avoid calling static methods directly
  • Avoid constructors that contain logic: it will be difficult for you to test this.


Work with legacy code

By “inherited” we mean code without tests. The quality of such a code may vary. Some tips on how to cover it with tests.

Architecture is testable

We are lucky there are no direct creations of classes and a meat grinder, and SOLID principles are respected. There is nothing easier - we create test projects, and step by step we cover the application using the principles described in the article. As a last resort, we will have to add a couple of setters for factories and select several interfaces.

Architecture is not testable

We have tight ties, crutches and other pleasures of life. We have to refactor. How to conduct complex refactoring correctly is a topic that goes far beyond the scope of this article.
It is worth highlighting the basic rule. If you do not change the interfaces - everything is simple, the procedure is identical. But if you are planning big changes, you should create a dependency graph and break your code into separate smaller subsystems (I hope this is possible). Ideally, it should look something like this: kernel, module # 1, module # 2, etc.
After that, select the victim. Just don't start with the kernel. First, take something smaller: what you can refactor in a reasonable amount of time. Cover this subsystem with integration and / or acceptance tests. And when you're done, you can cover this part with unit tests. Sooner or later, step by step, you must succeed.
Be prepared that this is likely to fail quickly . You will have to show strong-willed qualities.

Test support




Do not treat your tests as second-rate code. Many novice developers mistakenly believe that DRY, KISS and everything else is for production. And in tests everything is permissible. This is not true. Tests are the same code. The only difference is that the tests have a different goal - to ensure the quality of your application. All the principles used in the development of production code can and should be applied when writing tests.
There are only three reasons why the test fails:

  1. Error in the production code: this is a bug, you need to get it in the bug tracker and fix it.
  2. Bug in the test: apparently, the production code has changed, and the test is written with an error (for example, it tests too much or not what was needed). It is possible that before he went wrong. Understand and repair the test.
  3. Change of requirements. If the requirements have changed too much, the test should fall. This is correct and normal. You need to deal with the new requirements and fix the test. Or delete if it is no longer relevant.


Pay attention to supporting your tests, repair them on time, remove duplicates, highlight base classes and develop test APIs. You can create template base test classes that oblige you to implement a set of tests (for example, CRUD). If you do this regularly, then soon it will not take much time.

How to measure progress

To measure the success of implementing unit tests in your project, you should use two metrics:

  1. Number of bugs in new releases (including regression)
  2. Code coverage


The first one shows whether our actions have a result, or are we wasting time that we could spend on features. Second, how much more remains to be done.

The most popular tools for measuring code coverage on the .NET platform are:
  • NCover
  • dotTrace
  • Built-in Studio Test Coverage


Test first?




I deliberately did not touch on this topic until the very end. From my point of view, Test First is a good practice with a number of undeniable advantages. However, for one reason or another, sometimes I step back from this rule and write tests after the code is ready.

In my opinion, “how to write tests” is much more important than “when to do it”. Do as it suits you, but do not forget: if you start with tests, you get the architecture “in addition”. If you write code first, you may need to change it to make it testable.

Read on the topic

An excellent selection of links and books on the topic can be found in this article on Habré . I especially recommend The Art of Unit Testing. I read the first edition. It turns out that the second has come out.

Also popular now: