Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to keep Unit tests DRY and reducing Asserts

I am trying to implement a webserviceclass using the TDD approach, which sends out a bunch of webrequests and interprets the responses. I encapsulated the webrequests in several interfaces so that I can easily mock them. When requesting something via the webserviceclass the implemented method always returns a specific response object which contains an error object. With the help of this error object, the user can determine if a request was succesfull or not, and what the specific error was.

After writing a bunch of tests I realized that I repeated myself alot in the Arrange phase:

var mock = new Mock<ISomeWebservices>();
var sut = new MyWebServiceClass(mock.Object);
mock.Setup(foo=>foo.SomeRequest(someData)).Returns(@"{""user"": ""12345"",""somedata"": ""60"",""someotherdata"":""2015-09-01T12:00:00.200Z""}");
sut.SomeRequest(someData,s=> response = s);

The first 2 lines are always the same for all tests. There is always a Setup for the mock which returns something or throws an exception. Depending on which request I am testing I have to setup a different method ofcourse. I tried to solve that using Autofixture so that I can write an ICustomization for each webrequest, but the problem is that I still need access to the mock to have a test specific Setup.

The other problem is the Assert phase. Since I always get an error object from the request I am only asserting the error object if I am expection and error.

    Assert.That(response.Error.Type,Is.EqualTo(ErrorInfo.ErrorType.IllegalToken));
    Assert.That(response.Error.Message, Is.Not.Null);
    Assert.That(response.Error.AdvisedAction, Is.Not.Null);
    Assert.That(response.Error.RawData, Is.Not.Null);

So if there is an error, there are some properties of the error object which should always be filled out and some can be. (f.e. if the error was triggered by an exception, the exception property should be not null etc) As far as I understood, having several Asserts in a unit test is a bad practise so I would like to avoid that if possible.

[edit] According to the comment, having multiple Asserts as well as repeating the arrange parts I mentions, aren't that bad. So I won't use Autofixture for that.

like image 879
zlZimon Avatar asked Mar 15 '23 03:03

zlZimon


2 Answers

You most certainly can use AutoFixture to reduce the amount of boilerplate code you need in your Arrange phase. It could look like this:

[Theory, AutoMoqData]
public void Test(
    [Frozen]Mock<ISomeWebservices> mock,
    MyWebServiceClass sut,
    object someData,
    object response)
{
    mock.Setup(foo => foo.SomeRequest(someData)).Returns(@"{""user"": ""12345"",""somedata"": ""60"",""someotherdata"":""2015-09-01T12:00:00.200Z""}");
    sut.SomeRequest(someData, s => response = s);
    // Assertions go here...
}

Here, I had to guess at the type of someData and response, but if they're different from object, you simply declare them to be of that type instead.

In the above example, the [AutoMoqData] attribute is defined like this:

public class AutoMoqDataAttribute : AutoDataAttribute
{
    public AutoMoqDataAttribute() :
        base(new Fixture().Customize(new AutoMoqCustomization()))
    {
    }
}

All the other types and AutoFixture features are drawn from the AutoFixture.Xunit2 and AutoFixture.AutoMoq NuGet packages.

When it comes to multiple assertions, I agree with other commenters here that it doesn't look that bad, but in the spirit of GOOS, you ought to listen to your tests. In this particular case, the test seems to say: equality comparison for response looks like it's important. If that's the case, you might consider overriding Equals on response and give it Structural Equality instead of Reference Equality.

like image 76
Mark Seemann Avatar answered Mar 16 '23 16:03

Mark Seemann


You want to avoid duplication where ever possible, test code should be written to production code standard ideally, as you can spend as much time reading and maintaining test code as production code. Getting the balance is key, as you don't want brittle tests as pointed out than rely on each other.

In general use Test Data builders to simplify and remove duplication when creating test data & objects. This also helps if you make a change to how objects are created, as you only update one builder method rather than all the individual tests.

Multiple asserts per test are fine (probably essential), just ensure the asserts are relevant and the test sticks to a single concept, helping to identify errors quickly.

like image 24
MikeJ Avatar answered Mar 16 '23 17:03

MikeJ