Unit tests are code too. They can also get unwieldy and hard to read. Thus, they too can be refactored using the same principles used for regular code to eliminate clutter and make it easier to read and add new tests.
Let us consider a couple of scenarios:
Consider our ActivityDataTests
class from Unit Testing 4. We can see that the mock sql client and config objects are being created in every test.
public class ActivityDataTests
{
[Theory]
[InlineData(null)]
[InlineData("")]
public void Save_InvalidActivity_RaisesException(string activity)
{
// Arrange
var sqlClient = Mock.Of<ISqlClient>(); // <<==
var config = Mock.Of<IConfig>(); // <<==
var activityData = new ActivityData(sqlClient, config);
// Act, Assert
Assert.Throws<Exception>(() => activityData.Save(activity));
}
[Fact]
public void Save_ValidActivity_UpdatesDatabase()
{
// Arrange
var sqlClient = Mock.Of<ISqlClient>(); // <<==
Mock.Get(sqlClient)
.Setup(m => m.ExecuteNonQuery(It.IsAny<string>(), It.IsAny<string>()));
var config = Mock.Of<IConfig>(); // <<==
Mock.Get(config)
.SetupGet(p => p.ActivityDbConnectionString)
.Returns("valid-connection-string");
var activityData = new ActivityData(sqlClient, config);
// Act
activityData.Save("Create a compost pile");
// Assert
Mock.Get(sqlClient)
.Verify(m => m.ExecuteNonQuery(
It.IsAny<string>(), It.IsAny<string>()), Times.Once);
}
[Fact]
public void Save_InvalidConnectionString_RaisesException()
{
// Arrange
var sqlClient = Mock.Of<ISqlClient>(); // <<==
var config = Mock.Of<IConfig>(); // <<==
string invalidConnectionString = string.Empty;
Mock.Get(config)
.SetupGet(p => p.ActivityDbConnectionString)
.Returns(invalidConnectionString);
var activityData = new ActivityData(sqlClient, config);
// Act, Assert
Assert.Throws<Exception>(() => activityData.Save("Create a compost pile"));
}
}
This creation can be moved out into a constructor. xUnit creates a new instance of the test class for each test method so any expectations we setup will be on fresh instances of these objects and there is no potential for interference with other tests. Just by doing this we removed two lines of code per test:
public class RefactoredActivityDataTests
{
private readonly ISqlClient _sqlClient;
private readonly IConfig _config;
public RefactoredActivityDataTests()
{
_sqlClient = Mock.Of<ISqlClient>(); // <<==
_config = Mock.Of<IConfig>(); // <<==
}
[Theory]
[InlineData(null)]
[InlineData("")]
public void Save_InvalidActivity_RaisesException(string activity)
{
// Arrange
var activityData = new ActivityData(_sqlClient, _config);
// Act, Assert
Assert.Throws<Exception>(() => activityData.Save(activity));
}
[Fact]
public void Save_ValidActivity_UpdatesDatabase()
{
// Arrange
Mock.Get(_sqlClient)
.Setup(m => m.ExecuteNonQuery(It.IsAny<string>(), It.IsAny<string>()));
Mock.Get(_config)
.SetupGet(p => p.ActivityDbConnectionString)
.Returns("valid-connection-string");
var activityData = new ActivityData(_sqlClient, _config);
// Act
activityData.Save("Create a compost pile");
// Assert
Mock.Get(_sqlClient)
.Verify(m => m.ExecuteNonQuery(
It.IsAny<string>(), It.IsAny<string>()), Times.Once);
}
[Fact]
public void Save_InvalidConnectionString_RaisesException()
{
// Arrange
string invalidConnectionString = string.Empty;
Mock.Get(_config)
.SetupGet(p => p.ActivityDbConnectionString)
.Returns(invalidConnectionString);
var activityData = new ActivityData(_sqlClient, _config);
// Act, Assert
Assert.Throws<Exception>(() => activityData.Save("Create a compost pile"));
}
}
Setting up expectations on mock objects is also repetitive code that can get messy and hinder readability, especially when we have multiple dependencies and long parameter lists. In this case, we could extract out this code into mock wrapper classes:
public class MockSqlClient : Mock<ISqlClient>
{
public void Setup_ExecuteNonQuery()
{
this.Setup(m => m.ExecuteNonQuery(It.IsAny<string>(), It.IsAny<string>()));
}
public void Verify_ExecuteNonQuery_Called(Func<Times> times)
{
this.Verify(m => m.ExecuteNonQuery(
It.IsAny<string>(), It.IsAny<string>()), times);
}
}
public class MockConfig : Mock<IConfig>
{
public void SetupGet_ActivityDbConnectionString(string connectionString)
{
this.SetupGet(p => p.ActivityDbConnectionString)
.Returns(connectionString);
}
}
We could use it like this (note that we have to reference the .Object
property on the mock object to get the type being mocked). This yields a more concise and readable version of the test class:
public class RefactoredActivityDataTests
{
private readonly MockSqlClient _mockSqlClient;
private readonly MockConfig _mockConfig;
public RefactoredActivityDataTests()
{
_mockSqlClient = new MockSqlClient();
_mockConfig = new MockConfig();
}
[Theory]
[InlineData(null)]
[InlineData("")]
public void Save_InvalidActivity_RaisesException(string activity)
{
// Arrange
var activityData =
new ActivityData(_mockSqlClient.Object, _mockConfig.Object);
// Act, Assert
Assert.Throws<Exception>(() => activityData.Save(activity));
}
[Fact]
public void Save_ValidActivity_UpdatesDatabase()
{
// Arrange
_mockSqlClient.Setup_ExecuteNonQuery();
_mockConfig.SetupGet_ActivityDbConnectionString("valid-connection-string");
var activityData =
new ActivityData(_mockSqlClient.Object, _mockConfig.Object);
// Act
activityData.Save("Create a compost pile");
// Assert
_mockSqlClient.Verify_ExecuteNonQuery_Called(Times.Once);
}
[Fact]
public void Save_InvalidConnectionString_RaisesException()
{
// Arrange
_mockConfig.SetupGet_ActivityDbConnectionString(string.Empty);
var activityData =
new ActivityData(_mockSqlClient.Object, _mockConfig.Object);
// Act, Assert
Assert.Throws<Exception>(() => activityData.Save("Create a compost pile"));
}
}
https://github.com/cs31415/unit-test-samples