I read that singletons is bad for testability. Can you elaborate on this. Can't a singleton be mocked? They write due to tight coupling but I can't see to seem why.
It depends on how you're using the singleton and whether that singleton implements an interface. If the fact that it's a singleton is an implementation detail, then that's probably okay. If you're using its singleton nature in the code you want to test, that's more of a problem.
Take my Noda Time project for example. It has an IClock
interface representing "a way of getting the current time". That has an implementation called SystemClock
which is a singleton. Now consider some code using it:
public class BadClass
{
public bool IsIt2016Yet()
{
var now = SystemClock.Instance.Now;
return now.InUtc().Year >= 2016;
}
}
We can't test that code without somehow changing what SystemClock.Instance
does. That code is tightly coupled to the singleton class. However, consider this:
public class GoodClass
{
private readonly IClock clock;
public GoodClass(IClock clock)
{
this.clock = clock;
}
public bool IsIt2016Yet()
{
var now = clock.Now;
return now.InUtc().Year >= 2016;
}
}
Now you're fine - in your production code you set up dependency injection to use SystemClock.Instance
everywhere you want an IClock
, but in testing code you can use a fake clock - such as the one provided in the NodaTime.Testing
package. In production the code will end up using the singleton, but it's not tightly coupled to it.
That's assuming you have dependency injection suitably set up, of course. If you want to be able to just construct an instance anywhere without caring about the fact that it needs a clock, you could go for an option which optionally couples the class to the SystemClock
singleton:
public GoodClass(IClock clock)
{
this.clock = clock;
}
public GoodClass() : this(SystemClock.Instance)
{
}
Now it's still testable (and generally flexible) because you can pass in a clock, but it's convenient as it effectively "defaults" to the system clock. (You could do this with optional parameters and make an argument of null
end up using SystemClock.Instance
as another alternative.)
See more on this question at Stackoverflow