Reputation: 43
I am new to unit testing (xUnit) and I am not quite sure how to approach this method when writing tests for it.
Basically I have a class called Label.cs and in the constructor I use DI to inject 2 interfaces.
I also have a Get() method where it:
Is it possible to write tests for a method like this? I understand that I can mock the dependencies, pass them into the constructor and setup the methods using Moq but what if I had 4 or 5 more validations methods from ILabelValidation inside the Get() method? Would I need to setup all those methods using Moq?
public class Label
{
private readonly ICarrierService _carrierService;
private readonly ILabelValidation _validator;
public Label(int accountId, ICarrierService carrierService, ILabelValidation validator)
{
_carrierService = carrierService;
_validator = validator;
}
public override async Task<CreateShipmentResponse> Get(int accountId, Shipment shipment)
{
string xmlRequest = ConstructRequest(shipment);
// strip out any invalid characters inside the request
xmlRequest = _validator.InvalidCharacterValidation(xmlRequest);
// what if I have another method that I use from the ILabelValidation?
// _validator.ValidatePackageCount(3);
var stringContent = new StringContent(xmlRequest, Encoding.UTF8, "text/xml");
// make the API request
var shipmentResponse = await _carrierService.CreateShipment(dict, stringContent);
var stream = await shipmentResponse.Content.ReadAsStreamAsync();
Models.Label labelInfo = GetLabelInfo(stream);
var response = new CreateShipmentResponse();
response.labels = new List<string>();
if (!string.IsNullOrEmpty(labelInfo.Base64String))
response.labels.Add(labelInfo.Base64String);
return response;
}
}
Upvotes: 3
Views: 205
Reputation: 2913
As you have noted yourself, your method does multiple things. This is usually not a good thing. As you find it hard to test, it is a signal that your design needs more work.
In order to test things, it's best if method/component does as few things as possible. Ideally one thing.
Obviously, on the Label
level of abstraction, the one thing is Get
. However, as you drill down, you find out that you have to 1. Create a request, 2. Send the request, 3. Process the response. Now those are additional three steps that are one level of abstraction below but are separate and should be represented by methods on lower level of abstraction. Now these methods will probably be much easier to test, but may need further decomposition.
With this approach, you may choose to only have a couple of sanity tests for Label::Get
and detailed test suites covering all edge cases for <Your new request creation component>::CreateRequest
and <Your new response processing component>::ParseResponse
. The Label::Get
tests would probably need a spy double for the request verification with ICarrierService
and the CreateRequest
might also use a stub for ILabelValidation
. Although this really depends on your final design which may differ from my assumptions. If you're not sure about the doubles and how they match with Moq, you might want to check Martin Fowler's Mocks aren't stubs.
Please also note that doing things like Label::Get
may look very OOP and stuff, but if you find yourself having to mock out ICarrierService
when implementing unit test for some business logic with Label
s that is completey independent from how Label
s or CreateShipmentResponse
are acquired, your design has some more issues. This gets especially unpleasant if you add one more dependency to Label
and suddenly have to rework hundreds of unrelated tests. The idea of putting everything remotely related to one concept into one class leads over time to 5+kLoC legacy monsters that are extremely hard to change and test. But again, I don't know your system in detail so this is just for you to consider. To me, it just seems strange to have a constructor that takes int
, ICarrierService
and ILabelValidation
as those three things probably have completely different lifetimes and abstraction levels.
Upvotes: 2
Reputation: 1898
Unless you use MockBehaviour.Loose you have to create a setup for each call. Which makes sense in your scenario as you will need to provide data through the mock ups so you can make meaningful assertions on what the method should return.
With loose behaviour you don't need to provide setups but all method calls on the mocks will return the default value of the methods return type (null) which won't work with your code.
Upvotes: 0