Handling Multiple Mocks and Asserts in Unit Tests

谁说我不能喝 提交于 2019-12-06 07:33:28
Andrzej Gis

First of all, each test should only have one assertion (unless the other validates the real one) e.q. if you want to assert that all elements of a list are distinct, you may want to assert first that the list is not empty. Otherwise you may get a false positive. In other cases there should only be one assert for each test. Why? If the test fails, it's name tells you exactly what is wrong. If you have multiple asserts and the first one fails you don't know if the rest was ok. All you know than is "something went wrong".

You say you don't want to setup all mocks/stubs in 10 tests. This is why most frameworks offer you a Setup method which runs before each test. This is where you can put most of your mocks configuration in one place and reuse it. In NUnit you just create a method and decorate it with a [SetUp] attribute.

If you want to test a method with different values of a parameter you can use NUnit's [TestCase] attributes. This is very elegant and you don't have to create multiple identical tests.

Now lets talk about the useful tools.

AutoFixture this is an amazing and very powerful tool that allows you to create an object of a class which requires multiple dependencies. It setups the dependencies with dummy mocks automatically, and allows you to manually setup only the ones you need in a particular test. Say you need to create a mock for UnitOfWork which takes 10 repositories as dependencies. In your test you only need to setup one of them. Autofixture allows you to create that UnitOfWork, setup that one particular repository mock (or more if you need). The rest of the dependencies will be set up automatically with dummy mocks. This saves you a huge amount of useless code. It is a little bit like an IOC container for your test.

It can also generate fake objects with random data for you. So e.q. the whole initialization of EntityModel.Document would be just one line

var repoResult = _fixture.Create<EntityModel.Document>();

Especially take a look at:

  • Create
  • Freeze
  • AutoMockCustomization

Here you will find my answer explaining how to use AutoFixture.

SemanticComparison Tutorial This is what will help you to avoid multiple assertions while comparing properties of objects of different types. If the properties have the same names it will to it almost automatically. If not, you can define the mappings. It will also tell you exactly which properties do not match and show their values.

Fluent assertions This just provides you a nicer way to assert stuff. Instead of

Assert.AreEqual(repoResult.Message, savedDocument.Message);

You can do

repoResult.Message.Should().Be(savedDocument.Message);

To sum up. These tools will help you create your test with much less code and will make them much more readable. It takes time to get to know them well. Especially AutoFixture, but when you do, they become first things you add to your test projects - believe me :). Btw, they are all available from Nuget.

One more tip. If you have problems with testing a class it usually indicates a bad architecture. The solution usually is to extract smaller classes from the problematic class. (Single Responsibility Principal) Than you can easily test the small classes for business logic. And easily test the original class for interactions with them.

Consider using anonymous types:

public void Save_ReturnSavedDocument()
{
    // (unmodified code)...

    //Assert that properties are correctly mapped after save
    Assert.AreEqual(
        new
        {
            repoResult.Message,
            repoResult.DocumentId,
            repoResult.Name,
            repoResult.Email,
            repoResult.Comment,
        },
        new
        {
            savedDocument.Message,
            savedDocument.DocumentId,
            savedDocument.Name,
            savedDocument.Email,
            savedDocument.Comment,
        });
}

There is one thing to look-out for: nullable types (eg. int?) and properties that might have slightly different types (float vs double) - but you can workaround this by casting properties to specific types (eg. (int?)repoResult.DocumentId ).

Another option would be to create a custom assert class/method(s).

Basically, the trick is to push as much clutter as you can outside of the unittests, so that only the behaviour that is to be tested remains.

Some ways to do that:

  1. Don't declare instances of your model/poco classes inside each test, but rather use a static TestData class that exposes these instances as properties. Usually these instances are useful for more than one test as well. For added robustness, have the properties on the TestData class create and return a new object instance every time they're accessed, so that one unittest cannot affect the next by modifying the testdata.

  2. On your testclass, declare a helper method that accepts the (usually mocked) repositories and returns the system-under-test (or "SUT", i.e. your service). This is mainly useful in situations where configuring the SUT takes more than 2 or more statements, since it tidies up your test code.

  3. As an alternative to 2, have your testclass expose properties for each of the mocked Repositories, so that you don't need to declare these in your unittests; you can even pre-initialize them with a default behaviour to reduce the configuration per unittest even further.
    The helper method that returns the SUT then doesn't take the mocked Repositories as arguments, but rather contstructs the SUT using the properties. You might want to reinitialize each Repository property on each [TestInitialize].

  4. To reduce the clutter for comparing each property of your Poco with the corresponding property on the Model object, declare a helper method on your test class that does this for you (i.e. void AssertPocoEqualsModel(Poco p, Model m)). Again, this removes some clutter and you get the reusability for free.

  5. Or, as an alternative to 4, don't compare all properties in every unittest, but rather test the mapping code in only one place with a separate set of unittests. This has the added benefit that, should the mapping ever include new properties or change in any other way, you don't have to update 100-odd unittests.
    When not testing the property mappings, you should just verify that the SUT returns the correct object instances (i.e. based on Id or Name), and that just the properties that might be changed (by the business logic being currently tested) contain the correct values (such as Order total).

Personally, I prefer 5 because of its maintainability, but this isn't always possible and then 4 is usually a viable alternative.

Your test code would then look like this (unverified, just for demonstration purposes):

[TestClass]
public class DocumentServiceTest
{
    private IDocumentRepository DocumentRepositoryMock { get; set; }

    [TestInitialize]
    public void Initialize()
    {
        DocumentRepositoryMock = MockRepository.GenerateStub<IDocumentRepository>();
    }

    [TestMethod]
    public void Save_ReturnSavedDocument()
    {
        //Arrange
        var repoResult = TestData.AcmeDocumentEntity;

        DocumentRepositoryMock
            .Stub(m => m.Get())
            .IgnoreArguments()
            .Return(new List<EntityModel.Document>() { repoResult }.AsQueryable());

        DocumentRepositoryMock
            .Stub(a => a.Save(null, null))
            .IgnoreArguments()
            .Return(repoResult);

        //Act
        var documentService = CreateDocumentService();
        var savedDocument = documentService.Save(TestData.AcmeDocumentModel);

        //Assert that properties are correctly mapped after save        
        AssertEntityEqualsModel(repoResult, savedDocument);
    }

    //Helpers

    private DocumentService CreateDocumentService()
    {
        return new DocumentService(DocumentRepositoryMock);
    }

    private void AssertEntityEqualsModel(EntityModel.Document entityDoc, Models.Document modelDoc)
    {
        Assert.AreEqual(entityDoc.Message, modelDoc.Message);
        Assert.AreEqual(entityDoc.DocumentId, modelDoc.DocumentId);
        //...
    }
}

public static class TestData
{
    public static EntityModel.Document AcmeDocumentEntity
    {
        get
        {
            //Note that a new instance is returned on each invocation:
            return new EntityModel.Document()
            {
                DocumentId = 2,
                Message = "TestMessage1",
                //...
            }
        };
    }

    public static Models.Document AcmeDocumentModel
    {
        get { /* etc. */ }
    }
}

In general, if your having a hard time creating a concise test, your testing the wrong thing or the code your testing has to many responsibilities. (In my experience)

In specific, it looks like your testing the wrong thing here. If your repo is using entity framework, your getting the same object back that your sending in. Ef just updates to Id for new objects and any time stamp fields you might have.

Also, if you can't get one of your asserts to fail without a second assert failing, then you don't need one of them. Is it really possible for "name" to come back ok but for "email" to fail? If so, they should be in separate tests.

Finally, trying to do some tdd might help. Comment out all the could in your service.save. Then, write a test that fails. Then un comment out only enough code to make your test pass. Them write your next failing test. Can't write a test that fails? Then your done.

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