Вместо использования Singletons для установки реализаций компонентов используйте Dependency Injection и DI-библиотеку, например Ninject . Это именно тот сценарий, для которого они были разработаны.
Не подталкиваю вас к Ninject, но у них есть хороший учебник :) Концепции будут перенесены в другие фреймворки (например, Unity ).
С одним DI код будет выглядеть примерно так:
class Samurai {
private IWeapon _weapon;
public Samurai(IWeapon weapon) {
_weapon = weapon;
}
public void Attack(string target) {
_weapon.Hit(target);
}
}
class Shuriken : IWeapon {
public void Hit(string target) {
Console.WriteLine("Pierced {0}'s armor", target);
}
}
class Program {
public static void Main() {
Samurai warrior1 = new Samurai(new Shuriken());
Samurai warrior2 = new Samurai(new Sword());
warrior1.Attack("the evildoers");
warrior2.Attack("the evildoers");
}
}
Теперь это выглядит чисто, но подождите, пока у ваших зависимостей не появятся зависимости или что-то еще :) Вы можете использовать библиотеку DI для решения этой проблемы.
С библиотекой для управления подключением он будет выглядеть примерно так:
class Program {
public static void Main() {
using(IKernel kernel = new StandardKernel(new WeaponsModule()))
{
var samurai = kernel.Get<Samurai>();
warrior1.Attack("the evildoers");
}
}
}
// Todo: Duplicate class definitions from above...
public class WarriorModule : NinjectModule {
public override void Load() {
Bind<IWeapon>().To<Sword>();
Bind<Samurai>().ToSelf().InSingletonScope();
}
}
При любом из этих подходов, а также фреймворковой модели объектов, такой как Moq , ваши модульные тесты выглядят примерно так:
[Test]
public void HitShouldBeCalledByAttack()
{
// Arrange all our data for testing
const string target = "the evildoers";
var mock = new Mock<IWeapon>();
mock.Setup(w => w.Hit(target))
.AtMostOnce();
IWeapon mockWeapon = mock.Object;
var warrior1 = new Samurai(mockWeapon);
// Act on our code under test
warrior1.Attack(target);
// Assert Hit was called
mock.Verify(w => w.Hit(target));
}
Вы заметите, что можете просто передавать фиктивные экземпляры прямо в тестируемый код, и вам не нужно возиться с настройкой одиночных кодов. Это поможет вам избежать проблем, таких как необходимость установки состояния несколько раз или между вызовами. Это означает отсутствие скрытых зависимостей.
Вы также заметите, что я не использовал контейнер DI в тестах. Если ваш код хорошо продуман, он будет тестировать только один класс (и как можно чаще, только один метод), и вам нужно будет только смоделировать прямые зависимости этого класса.