I am cleaning up my code by way of removing duplicates, and found two classes that were almost identical, out of 55 lines, only a single predicate in an if
statement differed between them.
Both classes also had a suite of tests, which were almost identical copies as well.
The two original classes were FooApiAuthenticationProvider
and BarApiAuthenticationProvider
.
Below is my current refactored code using a template-pattern refactoring.
Template
public abstract class ApiAuthenticationProviderTemplate : IApiAuthenticationProvider
{
private readonly IClientRepository _clientRepository;
protected ApiAuthenticationProviderTemplate(IClientRepository clientRepository)
{
if (clientRepository == null)
{
throw new ArgumentNullException("clientRepository");
}
_clientRepository = clientRepository;
}
public GenericPrincipal GetPrincipal(string username, string password)
{
if (string.IsNullOrWhiteSpace(username))
{
throw new ArgumentNullException("username");
}
if (string.IsNullOrWhiteSpace(password))
{
throw new ArgumentNullException("password");
}
var client = clientRepository.GetByUsername(username);
if (client == null && !DoesPasswordMatch(password, client))
{
return null;
}
return PrincipalBuilder.BuildPrinciple(client.LoginIdentifier, client.ID);
}
protected abstract bool DoesPasswordMatch(string password, Client client);
}
FooApiAuthenticationProvider
public sealed class FooApiAuthenticationProvider : ApiAuthenticationProviderTemplate
{
public FooApiAuthenticationProvider(IClientRepository clientRepository)
: base(clientRepostitory)
{
}
protected override bool DoesPasswordMatch(string password, Client client)
{
return client.FooApiPassword == password;
}
}
BarApiAuthenticationProvider
public sealed class BarApiAuthenticationProvider : ApiAuthenticationProviderTemplate
{
public BarApiAuthenticationProvider(IClientRepository clientRepository)
: base(clientRepostitory)
{
}
protected override bool DoesPasswordMatch(string password, Client client)
{
return client.DoesBarServicePasswordMatch(password);
}
}
Both unit test fixtures for each specific class pass, and normally that would be the end of the thought, however, I have to extend the functionality that’s common to both authentication providers (so I would obviously implement in the template base class).
My questions are:
-
Should I refactor the test code to remove test duplication? To test the abstract class somehow, and write specific tests for the
DoesPasswordMatch
method -
Should I add new tests to both fixtures to ensure the actual types both work properly? Meaning the tests should have no idea the underlying class is a template, that way if both providers become significantly different where the template no longer makes sense, it could be refactored away without losing code coverage.
-
Should I leave both test fixtures the same, and just write a new fixture with tests against the template for the new functionality?
-
Is there something else I am not considering?
Remove the duplicate test code
Having tests on your child-classes for code that is in the parent class means twice the maintenance if the parent class changes. The reason you move to a parent class is specifically to avoid duplicating code, actual code as well as tests. (And if you ever end up creating a third child you might end up thinking that all tests on the parent class need to be duplicated AGAIN. So you can see where that leads you)
Test the parent class logic on the parent class, not through the children
Depending on the language you can use mocks, or write a specific implementation of the parent class that allows you to test the parent class methods (a TestableApiAuthenticationProviderTemplate). This would also test that when the preconditions for GetPrincipal are valid the DoesPasswordMatch-method is called.
Test your child classes independently
Each child class can have its own test class that only tests the code in the child. Note that you might have to change the signature for the DoesPasswordMatch to become public in order to facilitate testing. You could go through the parent method and test by calling GetPrincipal but you could end up doing a lot of setting up code so that it can reach the actual code you are testing. And again, when changing the code of the parent (because you want to check an additional parameter) you would have to change all the tests of the child classes.
1