Using DI container in unit tests

孤街浪徒 提交于 2019-11-27 14:28:17

For true unit tests (i.e. those which only test one class, and mock all of its dependencies), it doesn't make any sense to use a DI framework. In these tests:

  • if you find that you have a lot of repetitive code for newing up an instance of your class with all the mocks you've created, one useful strategy is to create all of your mocks and create the instance for the subject-under-test in your Setup method (these can all be private instance fields), and then each individual test's "arrange" area just has to call the appropriate Setup() code on the methods it needs to mock. This way, you end up with only one new PersonController(...) statement per test class.
  • if you're needing to create a lot of domain/data objects, it's useful to create Builder objects that start with sane values for testing. So instead of invoking a huge constructor all over your code, with a bunch of fake values, you're mostly just calling, e.g., var person = new PersonBuilder().Build(), possibly with just a few chained method calls for pieces of data that you specifically care about in that test. You may also be interested in AutoFixture, but I've never used it so I can't vouch for it.

If you're writing integration tests, where you need to test the interaction between several parts of the system, but you still need to be able to mock specific pieces, consider creating Builder classes for your services, so you can say, e.g. var personController = new PersonControllerBuilder.WithRealDatabase(connection).WithAuthorization(new AllowAllAuthorizationService()).Build().

If you're writing end-to-end, or "scenario" tests, where you need to test the whole system, then it makes sense to set up your DI framework, leveraging the same configuration code that your real product uses. You can alter the configuration slightly to give yourself better programmatic control over things like which user is logged in and such. You can still leverage the other builder classes you've created for constructing data, too.

var user = new PersonBuilder().Build();
using(Login.As(user))
{
     var controller = Container.Get<PersonController>();
     var result = controller.GetCurrentUser();
     Assert.AreEqual(result.Username, user.Username)
}

Refrain from using your DI container within your unit tests. In unit tests, you try to test one class or module in isolation, and there is little use for a DI container in that area.

Things are different with integration testing, since you want to test how the components in your system integrate and work together. In that case you often use your production DI configuration and swap out some of your services for fake services (such as your MailService) but stick as close to the real thing as you can. In this case you use your container to resolve the whole object graph.

The desire to use a DI container in the unit tests as well often stems from ineffective patterns. For instance, in case you try to create the class under test with all its dependencies in each test, you get lots of duplicated initialization code, and a little change in your class under test can in that case ripple through the system and require you to change dozens of unit tests. This obviously causes maintainability problems.

One pattern that helped me out here a lot in the past is the use of a simple test SUT-specific factory method. This method centralizes the creation of the class under test and minimizes the amount of changes that need to be made when the dependencies of the class under test change. This is how such factory method could look like:

private ClassUnderTest CreateClassUnderTest(
    ILogger logger = null, IMailSender mailSender = null,
    IEventPublisher publisher = null)
{
    return new ClassUnderTest(
        logger ?? new FakeLogger(),
        mailSender ?? new FakeMailer(),
        publisher ?? new FakePublisher());
}

This factory method arguments duplicate the class's constructor arguments, but makes them all optional. For any particular dependency that is not supplied by the caller, a new fake implementation will be injected.

This works very well, since in most tests you are just interested in one or two dependencies. The other dependencies might be required for the class to function, but are not interesting for that specific test. The factory method therefore allows you to only supply the dependencies that are interesting for the test at hand, while removing the noise of unused dependencies. The factory method therefore allows you to write the following test:

public void Test() {
    // Arrange
    var logger = new ListLogger();

    ClassUnderTest sut = CreateClassUnderTest(logger: logger);

    // Act
    sut.DoSomething();

    // Arrange
    Assert.IsTrue(logger.Count > 0);    
}

If you are interested in learning how to write Readable, Trustworthy and Maintainable (RTM) tests, I advise you to read Roy Osherove's book The Art of Unit Testing (second edition). This has helped me tremendously in my understanding of writing great unit tests. If you’re interested to learn more about dependency injection, my book is the most obvious book to read.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!