-
Notifications
You must be signed in to change notification settings - Fork 0
Unit Tests Implementation
You should postfix any unit test project name with .UnitTests
Why?
Using an explicit .UnitTests
name at the end of your unit test Visual Studio projects/assemblies ensures that developers know the appropriate location for their quick running unit tests.
It also allows you to easily identify the unit test projects to execute during a commit build. If test projects contained a mixture of unit and integration tests, it would be harder to differentiate between them to determine which to run on commit builds, and which tests to only run during a nightly build (as they take longer to run).
Unit test projects are also likely to have different dependencies to integration or acceptance tests, therefore having separate projects per test type ensures that you're reducing the required project dependencies.
TODO
The fastest way to get started with unit testing with a new test project is to install just three NuGet packages - AutoFixture.Xunit2
, AutoFixture.AutoMoq
, and xunit.assert
. Installing these three packages also installs all of the required and recommended test and isolation frameworks (XUnit.net 2, Moq, AutoFixture).
Note that the AutoFixture.Xunit2 NuGet package also installs
xUnit.net 2 as a (recommended) test framework
xUnit.net is the recommended test framework because of its extensibility and compatibility with AutoFixture.
Note that the AutoFixture.AutoMoq package also installs
Moq as a (recommended) isolation framework
Moq is the recommended isolation framework because of its fluent interface and compatibility with AutoFixture.
TODO
[Fact]
public void Test()
{
...
}
[Theory]
[InlineData(4)] // will pass
[InlineData(5)] // will fail
public void Test(int i)
{
Assert.Equal(4, i);
}
Note that the `AutoData` attribute is in the `Ploeh.AutoFixture.Xunit` namespace
[Theory, AutoData]
public void Test(Mock<IMyInterface> param)
{
...
}
The above test can be simplified by creating a custom AutoDataMoq
attribute that uses the AutoFixture AutoMoqCustomization
. This fixture customization is described as performing the following:
If a type falls through the normal engine without being handled, the auto-mocking
extension will check whether it is a request for an interface or abstract class.
If this is so, it will relay the request to a request for a `Mock` of the same type.
The attribute does not come as part of the AutoFixture.AutoMoq
NuGet package, so it does need implementing for each new product:
public class AutoMoqDataAttribute : AutoDataAttribute
{
public AutoMoqDataAttribute()
: base(new Fixture()
.Customize(new AutoMoqCustomization()))
{
}
}
Note that
AutoMoqCustomization
is defined in thePloeh.AutoFixture.AutoMoq
namespace
This means that the above unit test can be simplified to:
[Theory, AutoMoqData]
public void Test(IMyInterface param)
{
// param here will be a Moq proxy
}
[Theory]
[InlineAutoData(4, 5)]
public void Test(int x, int y, int z)
{
Assert.Equal(4, x); // passes
Assert.Equal(5, y); // passes
// The value for z will be auto-generated by AutoFixture
}
Note that the order of test parameters is important here, there is no way of not
specifying early parameters whilst specifying the values of later parameters.
If you wish to share a known instance of a type across many test methods, one option is to inject a factory into your test method:
public void Test(MyTypeFactory myTypeFactory)
{
var sut = myTypeFactory.Create();
...
}
However, you may prefer to inject the type directly into your test method, rather than explicitly invoking the factory. You may also need to access other (frozen) types
To allow for this, you can create a custom attribute and a factory base type:
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = true)]
public class CustomizeWithAttribute : CustomizeAttribute
{
private readonly Type type;
public CustomizeWithAttribute(Type type)
{
if (type == null)
{
throw new ArgumentNullException(nameof(type));
}
if (!typeof(ICustomization).IsAssignableFrom(type))
{
throw new InvalidOperationException($"Type must implement {typeof(ICustomization).Name}");
}
this.type = type;
}
public override ICustomization GetCustomization(ParameterInfo parameter)
{
return Activator.CreateInstance(this.type) as ICustomization;
}
}
public abstract class InstanceFactory<T> : ICustomization
{
private IFixture fixture;
public void Customize(IFixture fixture)
{
if (fixture == null)
{
throw new ArgumentNullException(nameof(fixture));
}
this.fixture = fixture;
this.fixture.Register(this.CreateInstance);
}
public abstract T CreateInstance();
protected TType CreateType<TType>()
{
return this.fixture.Create<TType>();
}
}
With these in place, you can create a factory for your type:
internal class MyTypeFactory : InstanceFactory<MyType>
{
public override MyType CreateInstance()
{
return new MyType(...);
}
}
And you can use your factory with the CustomizeWithAttribute
in your test method:
[Theory]
[AutoData]
public void Test([CustomizeWith(typeof(MyTypeFactory))]MyType myType)
{
...
}
If you need to access instances of other types injected into your test method from your factory, then you can use the provided CreateType<TType>
method:
internal class MyTypeFactory : InstanceFactory<MyType>
{
public override MyType CreateInstance()
{
return new MyType(this.CreateType<MyOtherType>());
}
}
And your test becomes:
[Theory]
[AutoData]
public void Test([Frozen]MyOtherType myOtherType, [CustomizeWith(typeof(MyTypeFactory))]MyType myType)
{
// myType will be constructed with myOtherType instance
}
You can simplify your test method even further by creating your own derived attribute for your specific injected type:
public class MyTypeFactoryAttribute : CustomizeWithAttribute
{
public MyTypeFactoryAttribute() : base(typeof(MyTypeFactory))
{
}
}
And your test becomes:
[Theory]
[AutoData]
public void Test([Frozen]MyOtherType myOtherType, [MyTypeFactory]MyType myType)
{
// myType will be constructed with myOtherType instance
}