📜 ⬆️ ⬇️

Simplify unit tests with the AutoFixture bundle and xUnit

We all know that unit tests are great, that only code that is somehow covered with tests can be trusted and that if some inexperienced senior developer senior programmer breaks something, the tests will immediately show it.

However, writing tests is hardly an exciting process. Initialization of test data, initialization of mocks, creation of a test object ... Until you get to the method call, which you actually wanted to test, test, and you don’t want anything. Of course, I'm exaggerating, by its nature a unit test should not take on too much and contain a couple of dozen initialization lines (although this happens), but writing the same code quickly gets boring. And now there are factories of test objects, a hierarchy of base classes for tests and other OOP gadgets, designed to "simplify" the creation of the test. As a rule, this leads to the fact that there is practically no chance to quickly understand what a test does without traveling around these very objects.

Actually, the tool that I want to talk about is precisely designed to simplify, and in some cases completely remove, the initialization phase or the Arrange test phase.

Consider this simple test:
')
[Fact] public void CanSaveEntity() { // Arrange var validEntity = new Entity() { Name = "Some Name", Type = Type.Simple, Group = new EntityGroup() { Name = "12345" } }; var validationServiceMock = new Mock<IValidationService>(); { validationServiceMock.Setup(svc => svc.IsValid(validEntity)).Returns(true); } var repositoryMock = new Mock<IRepository<Entity>>(); var loggingServiceMock = new Mock<ILoggingService>(); var sut = new ApplicationService(repositoryMock.Object, validationServiceMock.Object, loggingServiceMock.Object); // Act sut.SaveEntity(validEntity); // Assert repositoryMock.Verify(r => r.Add(validEntity), Times.Once); } 

Even in such a simple test, we have 6 lines of initialization. Uncover AutoFixture and voila:

  [Theory, AutoMoqData] public void CanSaveEntity(Entity validEntity, [Frozen]Mock<IValidationService> validationServiceMock, [Frozen]Mock<IRepository<Entity>> repositoryMock, ApplicationService sut) { // Arrange validationServiceMock.Setup(svc => svc.IsValid(validEntity)).Returns(true); // Act sut.SaveEntity(validEntity); // Assert repositoryMock.Verify(r => r.Add(validEntity), Times.Once); } 

The size of the Arrange phase has decreased by 6 times, it remains only to understand what is happening here.

Tool number 1 AutoFixture


In a nutshell, AutoFixture can create instances of different, generally arbitrarily complex types. Primitive types are created using built-in generators (for example, the string is a Guid.NewGuid prefixed). For user types, a constructor with a minimum number of parameters is selected, the value for each parameter is recursively generated by the same AutoFixture. In addition to this, AutoFixture will by default call all public setters of class properties, passing values ​​of self-generated objects as values. Thus, at the output we get an object filled with some random values. It looks suspicious: however, in most cases, some random values ​​like “TestName” and 42 are used as test data.

  [Fact] public void Generate() { // Old school var validEntity = new Entity() { Name = "Some Name", Type = Type.Simple, Group = new EntityGroup() { Name = "42" } }; // AutoFixture var someEntity = new Fixture().Create<Entity>(); } 

Of course, some properties that are important for the test and must have strictly defined values ​​can be set explicitly either after the creation of the object (if this can be done through a property or method), or when creating an object you can specify one or more rules to fill in specific properties:

  [Fact] public void Configure() { // Customize build var someEntity = new Fixture().Build<Entity>().With(e => e.Name, "Important For Test").Without(e => e.Group).Create(); } 

Another important advantage of this approach is the stability of tests to object changes. For example, if we add a couple of parameters to the constructor of the Entity object, then without AutoFixture we will need to run through a hundred or more tests, adjusting the constructor in each of them (well, or use Resharper). If we use AutoFixture, then we can do nothing at all (of course, if the values ​​of these parameters are not important for all these tests).

AutoFixture has quite powerful tuning capabilities that allow you to customize the generation of objects in cases where the default settings do not work, for example, in the case of tricky checks in constructors or setters.

It all looks very cool, but still quite verbose. For simple objects, the constructor call will be shorter than the AutoFixture construction. This is where xUnit data theories comes to our rescue.

Tool # 2 xUnit data theories


Again, in a nutshell, theories in xUnit are different from the facts in that the test method can be parameterized, and through the test attribute one way or another to tell with what parameters the test will be called. In the simple case, you can simply pass the values ​​explicitly, for example:

  [Theory] [InlineData(42)] [InlineData(3300)] public void Inline(int value) { Assert.True(value > 0); } 

But in combination with AutoFixture, which, as we already know, can generate anything, theories become even more powerful, allowing us to simply specify the list of incoming data in test parameters instead of creating them in the Arrange phase:

  [Theory, AutoData] public void TestEntity(Entity testEntity) { Assert.NotNull(testEntity); } 

Again, if the autogeneration of the entire object is not suitable for one reason or another, you can either redefine a part of the properties already generated in the test itself, or use the configuration mechanism both at the level of the entire attribute AutoMoqData and at the level of an individual parameter again using attributes. In AutoFixture, there are already several attributes that set up generation rules, however, you can easily write your own attributes that describe exactly your object generation logic.

  [Theory, AutoData] public void TestEntityWithoutProperties([NoAutoProperties]Entity testEntity) { Assert.Null(testEntity.Group); } 

For example, using the NoAutoProperties attribute, you can disable the initialization of properties for testEntity.
Fine! Arrange phase is less and less time to deal with the cumbersome code creating mocks. And here AutoFixture has a solution.

Tool number 3 AutoFixture + AutoMoqCustomization


I have already mentioned that AutoFixture allows you to customize yourself in every possible way. AutoMoqCustomization is one of the settings that “teaches” AutoFixture to do a simple thing: if none of the methods could create the required instance and the object type is either an interface or an abstract class, then AutoFixture creates moc using Moq. It sounds cool, we create a service through AutoFixture and all its dependencies are already initialized. One problem, sometimes moki need to configure and check what was caused there. Where can I get a copy of the moka? Here comes another AutoFixture mechanism called Freeze . Its essence is to memorize some specific value for the type of object and return it exactly (instead of generating a new one) every time it is needed when generating other objects.
For example, in this test:

  [Fact] public void Freeze() { var fixture = new Fixture(); fixture.Freeze<int>(42); var value = fixture.Create<int>(); Assert.Equal(42, value); } 

We say that if AutoFixture needs an int to create an object, you need to use 42 each time.
In the case of mocks, we use Freeze on a mock, and then when creating objects, this mock will be used whenever it is needed:

  [Fact] public void FreezeMock() { // Arrange var fixture = new Fixture().Customize(new AutoMoqCustomization()); var validEntity = fixture.Create<Entity>(); var repositoryMock = fixture.Freeze<Mock<IRepository<Entity>>>(); var validationServiceMock = fixture.Freeze<Mock<IValidationService>>(); { validationServiceMock.Setup(svc => svc.IsValid(validEntity)).Returns(true); } var sut = fixture.Create<ApplicationService>(); // Act sut.SaveEntity(validEntity); // Assert repositoryMock.Verify(r => r.Add(validEntity), Times.Once); } 

Again, I note that with this approach, adding a new dependency to the class constructor does not lead to changing all the tests where this object was created.

However, the code itself looks more verbose and even worse than the original test, where moki and service were created directly.
But we also have theories from xUnit.

Full arsenal: AutoFixture + Moq + xUnit + data theories


Here it is a test from the very beginning of the article, I hope, now it is more or less clear what is happening here:

  [Theory, AutoMoqData] public void CanSaveEntity(Entity validEntity, [Frozen]Mock<IValidationService> validationServiceMock, [Frozen]Mock<IRepository<Entity>> repositoryMock, ApplicationService sut) { // Arrange validationServiceMock.Setup(svc => svc.IsValid(validEntity)).Returns(true); // Act sut.SaveEntity(validEntity); // Assert repositoryMock.Verify(r => r.Add(validEntity), Times.Once); } 

A couple of nuances
AutoMoqData is a descendant of the standard attribute from AutoFixture AutoData , in which AutoMoqCustomization is applied.

Frozen is a standard attribute that, as you might guess, simply calls Freeze on the created object for the parameter.

In the end, it all works as a simple DI container, which also can create dependencies that no one has declared, by default.

I tried not to overload the text with details, the task of the article is to interest those who have not heard about AutoFixture before. In fact, AutoFixture is well documented, there are links to the documentation in Google and at the end of the article.

PS


A little about personal experience: I myself stumbled upon AutoFixture relatively recently, scrolling through the list of new courses PluralSite. Interested interested colleagues. At first I tried without xUnit, then I translated all the tests in the project from MSUnit to xUnit, in order to try with xUnit, and now I can hardly imagine how I used to write tests without it. It's like ReSharper, only AutoFixture - it's worth trying and soon you have little idea how you used to live without it.

Links


Auto-Fixture cheet-sheet
Mark Siman blog with a series of articles on AutoFixture

Source: https://habr.com/ru/post/262435/


All Articles