I'm struggling to come to a consensus on the right approach that can be used somewhat consistently to balance integration and unit testing.
Take the following method, that is extremely common to find in a C# .NET Core application.
public interface IBusinessLogicService
{
CalculationResult CalculateBusinessRuleResult(
string businessRuleName,
int input
);
}
public class BusinessLogicService: IBusinessLogicService
{
private readonly ICache _cache;
private readonly DbContext _context;
private readonly BusinessValidator _validator;
public BusinessLogicService(
ICache cache,
DbContext context,
BusinessValidator validator //injected
) {
_cache = cache;
_context = context;
_validator = validator;
}
public CalculationResult CalculateBusinessRuleResult(
string businessRuleName,
int input
) {
var hasRule = _cache.TryGet(businessRuleName, out BusinessRule rule);
if (!hasRule)
{
rule = _context.BusinessRules.First(f => f.Name == businessRuleName);
_cache.Add(businessRuleName, rule);
}
return ApplyRule(rule, intermediateValue);
}
internal CalculationResult ApplyRule(rule, input)
{
// Logic Line 1
// Logic Line 2
// Logic Line 3
return new CalculationResult();
}
}
In my mind, I identify CalculateBusinessRuleResult as an integration of parts (cache, database, and ApplyRule), and I view ApplyRule as a pure function. To me, this means that I should be unit testing ApplyRule and integration testing CalculatebusinessRuleResult.
My primary conflict is that I've seen it said many times that in this situation, ApplyRule is an implementation detail and should not be unit tested, as it is not directly exposed via the API Surface Area intended by the interface implementation. Furthermore there is an argument that if you have to mock dependencies, you are not unit testing, it is an integration test -- clearly visible by my dependency on the cache and database.
Is there really no unit-testable code here? To me, there is -- ApplyRule. A pure, functional business logic method.
This is the approach I've taken to a lot of code recently - declare dependency usage at the top of the public method, and then utilize those dependencies in units of business logic further down. Integration test this method, and then unit-test the bits of business logic below. This also lines up with the idea that an application should be produced of X number of end-to-end tests, +X number of integration tests, and ++X number of unit tests.
_context
in the same class._validator
doesn't exist. Where are you getting yourExternalDependency
instance from?ICache
andDbContext
and doing some logic with them inside some function, replace the two with aICachedRepository
that just hasGetBussinessRule(name)
(and maybe a couple of other app-logic specific functions that you need - you're not building a generic repository), and put the retrieval logic in there. Then you can fake/mock that. And your code becomes just:return ApplyRule(_cachedRepo.GetBussinesRule(name), input);
.