Fake Methods and Dependencies without Dependency Injection
# Mocking via virtual method
Fake Dependencies with an Interface and Dependency Injection is easy and the usual approach. But how can you fake without Dependency Injection or an Interface. I will show you two situations that came up in production code.
# Factory Method
The first situation is a small AWS Lambda function with an http call. We want to mock the .Net HttpClient and write a UnitTest for our Request method.
Here you can see a simple example:
public class RequestService
{
public async Task SendPostRequest()
{
var url = new Uri("https://test-uri.com");
var _client = new HttpClient();
var response = await _client.PostAsync(url, null);
}
}
The first thing we do is create a wrapper for the HttpClient.
public interface IHttpHandler
{
Task<HttpResponseMessage> PostAsync(Uri url, HttpContent content);
}
public class HttpClientHandler : IHttpHandler
{
private readonly HttpClient _client = new HttpClient();
public async Task<HttpResponseMessage> PostAsync(Uri url,
HttpContent content)
{
return await _client.PostAsync(url, content);
}
}
The second step is that we use a factory method to create an instance of the new HttpClientHandler
class. The important part here is that the method has to be virtual.
public class RequestService
{
public async Task SendPostRequest()
{
var url = new Uri("https://test-uri.com");
var httpHandler = GetHttpHandler();
var response = await httpHandler.PostAsync(url, null);
}
protected virtual IHttpHandler GetHttpHandler
=> new HttpClientHandler();
}
With this little refactoring we can start writing our Test. To take control over the IHttpHandler
we create a FakeRequestService
and override the factory method.
internal class FakeRequestService : RequestService
{
public IHttpHandler HttpHandler { get; set; }
protected override IHttpHandler GetHttpHandler => HttpHandler;
}
We can initialize the FakeRequestService
with our own IHttpHandler
. Now we have the control over IHttpHandler
and can check if our SendPostRequest
method called the PostAsync
method with the correct parameters.
public async Task PostRequestTest()
{
var httpHandler = A.Fake<IHttpHandler>();
var sut = new FakeRequestService { HttpHandler = httpHandler };
await sut.SendPostRequest();
var expectedUrl = new Uri("https://test-uri.com");
A.CallTo(() => httpHandler.PostAsync(expectedUrl, null))
.MustHaveHappenedOnceExactly();
}
# Avoid unnecessary complexity in test
The second use case was a big Product
model.
We consumed from the legacy system an ProductUpdated
event and made in our new ProductService
the decision that we will only execute an update in the database if the product really changed.
public class ProductService
{
public bool CompareAndUpdate(Product product)
{
//get Product from database
var existingProdcut = new Product();
if (product.Equals(existingProdcut))
{
//do Nothing
return false;
}
//update product
return true;
}
}
For our use case we decided to customize the Equals
method.
public class Product
{
public virtual bool Equals(Product other)
{
return other != null;
//&& do magic stuff
}
}
We wrote a lot of ProductTests
for the Equals
method.
When we startet to write ProductServiceTests
we didn't want to duplicate the test data from the ProductTests
to simulate the true
and false
behavior. Instead we decided to fake the Equals
result.
You can see that the Equals
method is again virtual.
That means we can create a ProductFake
and take control over the boolean
.
public class ProductFake : Product
{
public bool EqualState { get; set; }
public override bool Equals(Product other)
{
return EqualState;
}
}
Now we can easily write the ProductServiceTests
test and focus 100% an the real logic and avoid a lot of unnecessary test code.
public class ProductServiceTests
{
public void ProductShouldBeUpdates()
{
var product = new ProductFake() { EqualState = false };
var sut = new ServiceUnderTest();
var result = sut.CompareAndUpdateIfNecessary(product);
result.Should().BeTrue();
}
}
You can learn more about this topic in the book The Art of Unit Testing (opens new window) from Roy Osherove.