J'ai une classe qui a un membre privé qui a pour type System.Windows.Forms.Timer
. Il y a aussi une méthode privée qui est appelée à chaque fois que mon chronomètre se déclenche.
éditer:
En fait, la méthode dépend du timing, voici le code:
private void alertTick(object sender, EventArgs e) {
if (getRemainingTime().Seconds <= 0) {
Display.execute(Name, WarningState.Ending, null);
AlertTimer.Stop();
}
else {
var warning = _warnings.First(x => x == getRemainingTime());
if (warning.TotalSeconds > 0)
Display.execute(Name, WarningState.Running, warning);
}
}
Comme vous pouvez le voir, si le temporisateur est en cours d'exécution, il appelle Display.execute()
avec des paramètres différents de sa fin (lorsque le temps restant est égal à 0). Serait-ce un problème de conception?
En fait, votre classe a trop de responsabilités - l'une planifie une tâche et l'autre - exécute certaines actions. Essayez de diviser votre classe en deux classes distinctes avec responsabilités uniques .
Ainsi, la planification va au planificateur :) L'API du planificateur pourrait ressembler à:
public interface IScheduler
{
event EventHandler<SchedulerEventArgs> Alarm;
void Start();
void Stop();
}
Oubliez le planificateur pour l'instant. Retournez et implémentez votre deuxième classe, qui affichera quelques avertissements. Allons tester d'abord (avec Moq):
[Test]
public void ShouldStopDisplayingWarningsWhenTimeIsOut()
{
Mock<IDisplay> display = new Mock<IDisplay>();
Mock<IScheduler> scheduler = new Mock<IScheduler>();
Foo foo = new Foo("Bar", scheduler.Object, display.Object);
scheduler.Raise(s => s.Alarm += null, new SchedulerEventArgs(0));
display.Verify(d => d.Execute("Bar", WarningState.Ending, null));
scheduler.Verify(s => s.Stop());
}
Ecrire l'implémentation:
public class Foo
{
private readonly IScheduler _scheduler;
private readonly IDisplay _display;
private readonly string _name;
public Foo(string name, IScheduler scheduler, IDisplay display)
{
_name = name;
_display = display;
_scheduler = scheduler;
_scheduler.Alarm += Scheduler_Alarm;
_scheduler.Start();
}
private void Scheduler_Alarm(object sender, SchedulerEventArgs e)
{
_display.Execute(_name, WarningState.Ending, null);
_scheduler.Stop();
}
}
Le test réussit. Écrivez-en un autre:
[Test]
public void ShouldNotStopDisplayingWarningsWhenTimeRemains()
{
Mock<IDisplay> display = new Mock<IDisplay>(MockBehavior.Strict);
Mock<IScheduler> scheduler = new Mock<IScheduler>(MockBehavior.Strict);
scheduler.Setup(s => s.Start());
Foo foo = new Foo("Bar", scheduler.Object, display.Object);
scheduler.Raise(s => s.Alarm += null, new SchedulerEventArgs(1));
}
Test échoué. Ah, vous avez besoin de condition pour le temps restant:
private void Scheduler_Alarm(object sender, SchedulerEventArgs e)
{
if (e.RemainingTime > 0)
return;
_display.Execute(_name, WarningState.Ending, null);
_scheduler.Stop();
}
Vous pouvez continuer à écrire des tests pour votre classe, qui est chargée de gérer les alertes du planificateur et d'exécuter certains avertissements à l'écran. Lorsque vous avez terminé, vous pouvez écrire l'implémentation de votre interface IScheduler
. Peu importe la façon dont vous implémenterez la planification - via System.Windows.Forms.Timer ou via System.ThreadingTimer, ou d'une autre manière.
Vaut-il la peine de tester la méthode? (puisque c'est privé)
Votre but est de décider si votre code fonctionne ou non. Même s'il s'agit d'une méthode privée, elle devrait générer une sortie accessible par l'interface publique. Vous devez concevoir votre classe de manière à ce que l'utilisateur sache si cela fonctionne ou non.
De plus, lorsque vous testez l'unité, le rappel affecté à l'événement Elapsed du minuteur est accessible si vous pouvez vous moquer du minuteur.
Comment puis-je le tester? (Je sais que ma classe de test peut hériter de la classe que je veux tester ...)
Vous pouvez utiliser une classe d'adaptateur ici. Vous devez d'abord définir une abstraction car la classe Timer n'en propose pas.
public interface ITimer
{
void Start();
void Stop();
double Interval { get; set; }
event ElapsedEventHandler Elapsed;
//and other members you need
}
Ensuite, vous pouvez implémenter cette interface dans une classe d'adaptateur, héritant simplement de la classe Timer.
public class TimerAdaper : Timer, ITimer { }
Vous devez injecter votre abstraction dans le constructeur (ou en tant que propriété) afin de pouvoir vous en moquer dans vos tests.
public class MyClass
{
private readonly ITimer _timer;
public MyClass(ITimer timer)
{
_timer = timer
}
}
Dois-je me moquer de ma minuterie? Parce que si je dois tester une classe qui utilise une minuterie interne, mes tests peuvent prendre beaucoup de temps, non?
Bien sûr, vous devriez vous moquer de votre minuterie. Vos tests unitaires ne peuvent pas dépendre de l'heure du système. Vous devez déclencher des événements en vous moquant et voir comment votre code se comporte.