There are many well-known benefits of testing your code, but for many developers, testing is still the part of their job they like the least. They find it hard and painful, but it doesn't have to be that way.
Nowadays, there's excellent tooling to make it fun and productive, and that's what this blog post is all about.
xUnit, AutoFixture, and FakeItEasy are among my favorites frameworks/tools. They all achieve something particular, and they all play well together. I won't go over the details for each one, because there's a ton of documentation with great examples out there, but here's a quick look at what they do.
xUnit
xUnit.net is a free, open-source, community-focused unit testing tool for the .NET Framework. Written by the original inventor of NUnit v2, xUnit.net is the latest technology for unit testing C#, F#, VB.NET, and other .NET languages. xUnit.net works with ReSharper, CodeRush, TestDriven.NET, and Xamarin.
AutoFixture
AutoFixture is an open-source library for .NET designed to minimize the 'Arrange' phase of your unit tests to maximize maintainability. Its primary goal is to allow developers to focus on what is being tested rather than how to set up the test scenarios by making it easier to create object graphs containing test data.
FakeItEasy
FakeItEasy is a .Net dynamic fake framework for creating all types of fake objects, mocks, stubs, etc.
In this post, what I'm more interested in is to show you how to integrate them. Let's do it with a hands-on example that we're going to complete together.
First of all, install the following NuGet packages in your test project.
- xunit
- AutoFixture
- FakeItEasy
- AutoFixture.Xunit2
- AutoFixture.FakeItEasy
Class to test
Second of all, let's define a simple example of a class to test a.k.a. System Under Test (sut)
public class StringService
{
private INumberService _service;
public StringService(INumberService service)
{
this._service = service;
}
public int ConvertToNumber(string value)
{
// Service that returns a number for each char in a string
// corresponding to that letter position in the alphabet
var numbers = this._service.Execute(value);
return numbers.Sum();
}
}
Test - V1
[Theory]
[InlineData(6, [1,2,3], "abc")]
[InlineData(15, [4,5,6], "def")]
public void GivenAString_WhenConvertToNumber_ThenSumOfLetterPositionIsReturned(int expected,
int[] charValues, string value)
{
// Given
var service = A.Fake<INumberService>();
A.CallTo(() => service.Execute(A<string>._).Returns(charValues);
var sut = new StringService(service);
// When
var result = sut.ConvertToNumber(value);
// Then
Assert.Equal(expected, result);
}
Pretty simple, it's neither too big nor dirty. We are leveraging [Theory] and [InlineData]
to get a helpful, clean test, but it seems like we might miss some test cases.
Test - V2
Let's change the test to use [AutoData]
in order to generate random test data and cover more edge cases
[Theory]
[AutoData]
[InlineAutoData([1,2,3], "abc")]
[InlineAutoData([4,5,6], "def")]
public void GivenAString_WhenConvertToNumber_ThenSumOfLetterPositionIsReturned(int[] charValues,
string value)
{
// Given
var fixture = new Fixture();
var service = A.Fake<INumberService>();
A.CallTo(() => service.Execute(A<string>._).Returns(charValues);
fixture.Register<INumberService>(()=> service);
var sut = fixture.Create<StringService>();
// When
var result = sut.ConvertToNumber(value);
// Then
var expected = charValues.Sum();
Assert.Equal(expected, result);
}
It's nice to have some auto-generated test data, but the Given part of the test is getting quite big and harder to understand. We can probably do something with this.
Test - Final Version
Let's use everything in the toolbox, but to do so, we'll need to create our integration between the frameworks.
Here's the actual implementation of the [AutoDataFakeItEasy]
and [InlineAutoDataFakeItEasy]
attributes that will make all of the heavy lifting for you.
Magic Attributes
[AttributeUsage(AttributeTargets.Method)]
public class AutoFakeItEasyDataAttribute : AutoDataAttribute
{
public AutoFakeItEasyDataAttribute()
: base(FixtureFactory)
{
}
private static IFixture FixtureFactory()
{
return new Fixture().Customize(new AutoFakeItEasyCustomization());
}
}
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
public class InlineAutoFakeItEasyDataAttribute : InlineAutoDataAttribute
{
private readonly object[] _values;
public InlineAutoFakeItEasyDataAttribute(params object[] values)
: base(new AutoFakeItEasyDataAttribute(), values)
{
this._values = values;
}
public override IEnumerable<object[]> GetData(MethodInfo testMethod)
{
var data = base.GetData(testMethod).ToList();
for (var i = 0; i < this._values.Length; i++)
{
data[0][i] = this._values[i];
}
return data;
}
}
The final test
[Theory]
[AutoDataFakeItEasy]
[InlineAutoDataFakeItEasy([1,2,3])]
[InlineAutoDataFakeItEasy([4,5,6])]
public void GivenAString_WhenConvertToNumber_ThenSumOfLetterPositionIsReturned(int[] charValues,
string value, [Frozen]INumberService service, StringService sut)
{
// Given
A.CallTo(() => service.Execute(A<string>._).Returns(charValues);
// When
var result = sut.ConvertToNumber(value);
// Then
var expected = charValues.Sum();
Assert.Equal(expected, result);
}
We still get all the benefits of auto-generated test data and the flexibility to add our specific test cases. The best part, we got rid of all the boilerplate code that was initializing our sut and its dependencies. The attributes will make sure to initialize our test values with inputs from the attribute or random data otherwise. They'll then make sure to instantiate mocks and dummy classes, also taking care of all their dependencies. It gives us a very robust and maintainable test that will survive many refactors without needing any changes.
The only thing we need to define now is the behavior of the mock, calling the method under test and asserting the result. Wow, this is what every test should look like. Like we say: less is more.
Conclusion
It is a very simple sut and test. Imagine when you have a complex class to test, how much code and complexity you can save with these attributes.
As you can see, knowing the right tools can make you super efficient at writing good unit tests. Moreover, your tests will be simple to understand and maintain over time.
Top comments (0)