paint-brush
Der beste Weg, Timer in .NET C# zu verwendenvon@ahmedtarekhasan
4,384 Lesungen
4,384 Lesungen

Der beste Weg, Timer in .NET C# zu verwenden

von Ahmed Tarek Hasan30m2023/03/29
Read on Terminal Reader
Read this story w/o Javascript

Zu lang; Lesen

Wenn Sie System.Timers.Timer in Ihrer C#-Anwendung verwenden, könnten Sie Probleme mit der Abstraktion und der Möglichkeit haben, Ihre Module mit Unit-Tests abzudecken. In diesem Artikel diskutieren wir die Best Practices zur Bewältigung dieser Herausforderungen. Am Ende können Sie eine 100-prozentige Abdeckung Ihrer Module erreichen.
featured image - Der beste Weg, Timer in .NET C# zu verwenden
Ahmed Tarek Hasan HackerNoon profile picture

So haben Sie die volle Kontrolle über den Timer und können mit Unit-Tests eine 100-prozentige Abdeckung erreichen

Wenn Sie System.Timers.Timer in Ihrer .NET C# -Anwendung verwenden, treten möglicherweise Probleme bei der Abstraktion und der Möglichkeit auf, Ihre Module mit Komponententests abzudecken.


In diesem Artikel diskutieren wir die Best Practices , wie Sie diese Herausforderungen meistern können, und am Ende werden Sie in der Lage sein, eine 100-prozentige Abdeckung Ihrer Module zu erreichen.


Foto von Lina Trochez auf Unsplash

Die Vorgehensweise

So gehen wir unsere Lösung an:


  1. Überlegen Sie sich ein sehr einfaches Beispiel, an dem Sie arbeiten können.


  2. Beginnen Sie mit der einfachen schlechten Lösung.


  3. Versuchen Sie weiter, es zu verbessern, bis wir das endgültige Format erreicht haben.


  4. Zusammenfassung der Erkenntnisse unserer Reise.


Foto von James Harrison auf Unsplash

Das Beispiel

In unserem Beispiel erstellen wir eine einfache Konsolenanwendung , die nur eine einfache Sache tun würde: Verwenden Sie einen System.Timers.Timer , um jede Sekunde Datum und Uhrzeit in die Konsole zu schreiben.


Am Ende sollten Sie Folgendes erhalten:


Bild von Ahmed Tarek


Wie Sie sehen, sind die Anforderungen einfach, nichts Besonderes.


Foto von Mikael Seegen auf Unsplash, angepasst von Ahmed Tarek

Haftungsausschluss

  1. Einige Best Practices werden ignoriert/verworfen, um den Schwerpunkt auf die anderen Best Practices zu lenken, auf die in diesem Artikel abzielt.


  2. In diesem Artikel konzentrieren wir uns darauf, das Modul mithilfe von System.Timers.Timer mit Komponententests abzudecken. Der Rest der Lösung würde jedoch nicht durch Unit-Tests abgedeckt. Wenn Sie mehr darüber erfahren möchten, können Sie den Artikel lesen So decken Sie die .NET C#-Konsolenanwendung vollständig mit Komponententests ab .


  3. Es gibt einige Bibliotheken von Drittanbietern, mit denen sich nahezu ähnliche Ergebnisse erzielen lassen. Allerdings würde ich, wann immer möglich, lieber einem nativen, einfachen Design folgen, als mich auf eine ganze große Bibliothek eines Drittanbieters zu verlassen.


Foto von Maria Teneva auf Unsplash, angepasst von Ahmed Tarek

Schlechte Lösung

In dieser Lösung würden wir System.Timers.Timer direkt verwenden, ohne eine Abstraktionsebene bereitzustellen.


Der Aufbau der Lösung sollte wie folgt aussehen:


Bild von Ahmed Tarek


Es handelt sich um eine UsingTimer- Lösung mit nur einem Console TimerApp- Projekt.


Ich habe absichtlich etwas Zeit und Mühe in die Abstraktion System.Console in IConsole investiert, um zu beweisen, dass dies unser Problem mit dem Timer nicht lösen würde.


 namespace TimerApp.Abstractions { public interface IConsole { void WriteLine(object? value); } }


In unserem Beispiel müssten wir nur System.Console.WriteLine verwenden; Deshalb ist dies die einzige abstrahierte Methode.


 namespace TimerApp.Abstractions { public interface IPublisher { void StartPublishing(); void StopPublishing(); } }


Wir haben nur zwei Methoden auf der IPublisher Schnittstelle: StartPublishing und StopPublishing .


Nun zu den Implementierungen:


 using TimerApp.Abstractions; namespace TimerApp.Implementations { public class Console : IConsole { public void WriteLine(object? value) { System.Console.WriteLine(value); } } }


Console ist nur ein dünner Wrapper für System.Console .


 using System.Timers; using TimerApp.Abstractions; namespace TimerApp.Implementations { public class Publisher : IPublisher { private readonly Timer m_Timer; private readonly IConsole m_Console; public Publisher(IConsole console) { m_Timer = new Timer(); m_Timer.Enabled = true; m_Timer.Interval = 1000; m_Timer.Elapsed += Handler; m_Console = console; } public void StartPublishing() { m_Timer.Start(); } public void StopPublishing() { m_Timer.Stop(); } private void Handler(object sender, ElapsedEventArgs args) { m_Console.WriteLine(args.SignalTime); } } }


Publisher ist eine einfache Implementierung von IPublisher . Es verwendet einen System.Timers.Timer und konfiguriert ihn nur.


Die IConsole ist als Abhängigkeit definiert. Dies ist aus meiner Sicht keine Best Practice. Wenn Sie verstehen möchten, was ich meine, können Sie den Artikel lesen Wann DI-, IoC- und IoC-Container in .NET C# nicht verwendet werden sollten .


Der Einfachheit halber würden wir es jedoch einfach als Abhängigkeit in den Konstruktor einfügen.


Wir setzen außerdem das Timer-Intervall auf 1000 Millisekunden (1 Sekunde) und stellen den Handler so ein, dass er die Timer- SignalTime in die Konsole schreibt.


 using TimerApp.Abstractions; using TimerApp.Implementations; namespace TimerApp { public class Program { static void Main(string[] args) { IPublisher publisher = new Publisher(new Implementations.Console()); publisher.StartPublishing(); System.Console.ReadLine(); publisher.StopPublishing(); } } }


Hier in der Program machen wir nicht viel. Wir erstellen gerade eine Instanz der Publisher Klasse und beginnen mit der Veröffentlichung.


Wenn Sie dies ausführen, sollte dies etwa so aussehen:


Bild von Ahmed Tarek


Die Frage ist nun: Was können Sie tun, wenn Sie einen Komponententest für die Publisher Klasse schreiben möchten?


Leider wäre die Antwort: nicht zu viel .


Erstens fügen Sie den Timer selbst nicht als Abhängigkeit ein. Das bedeutet, dass Sie die Abhängigkeit innerhalb der Publisher Klasse verbergen. Daher können wir den Timer nicht verspotten oder stoppen.


Nehmen wir zweitens an, dass wir den Code so geändert haben, dass der Timer jetzt in den Konstruktor eingefügt wird. Dennoch wäre die Frage, wie man einen Unit-Test schreibt und den Timer durch einen Mock oder Stub ersetzt.


Ich höre jemanden schreien: „Lasst uns den Timer in eine Abstraktion einbinden und ihn anstelle des Timers einfügen.“


Ja, das stimmt, allerdings ist es nicht so einfach. Es gibt einige Tricks, die ich im nächsten Abschnitt erklären werde.


Foto von Carson Masterson auf Unsplash, angepasst von Ahmed Tarek

Gute Lösung

Jetzt ist die Zeit für eine gute Lösung. Mal sehen, was wir dagegen tun können.


Der Aufbau der Lösung sollte wie folgt aussehen:


Bild von Ahmed Tarek


Es handelt sich um die gleiche UsingTimer- Lösung mit einem neuen Console BetterTimerApp- Projekt.


IConsole , IPublisher und Console wären identisch.

ITimer

 using System; namespace BetterTimerApp.Abstractions { public delegate void TimerIntervalElapsedEventHandler(object sender, DateTime dateTime); public interface ITimer : IDisposable { event TimerIntervalElapsedEventHandler TimerIntervalElapsed; bool Enabled { get; set; } double Interval { get; set; } void Start(); void Stop(); } }


Was wir hier bemerken können:


  1. Wir haben den neuen Delegaten TimerIntervalElapsedEventHandler definiert. Dieser Delegat stellt das Ereignis dar, das von unserem ITimer ausgelöst werden soll.


  2. Sie könnten argumentieren, dass wir diesen neuen Delegaten nicht benötigen, da wir bereits über den nativen ElapsedEventHandler verfügen, der bereits von System.Timers.Timer verwendet wird.


  3. Ja, das stimmt. Sie werden jedoch feststellen, dass das ElapsedEventHandler Ereignis ElapsedEventArgs als Ereignisargumente bereitstellt. Dieses ElapsedEventArgs verfügt über einen privaten Konstruktor und Sie könnten keine eigene Instanz erstellen. Darüber hinaus ist die in der ElapsedEventArgs -Klasse definierte SignalTime Eigenschaft schreibgeschützt. Daher können Sie es in einer untergeordneten Klasse nicht überschreiben.


  4. Für Microsoft wurde ein Änderungsanforderungsticket zur Aktualisierung dieser Klasse geöffnet, aber bis zum Zeitpunkt der Erstellung dieses Artikels wurden keine Änderungen vorgenommen.


  5. Beachten Sie außerdem, dass ITimer das IDisposable erweitert.



Herausgeber

 using System; using BetterTimerApp.Abstractions; namespace BetterTimerApp.Implementations { public class Publisher : IPublisher { private readonly ITimer m_Timer; private readonly IConsole m_Console; public Publisher(ITimer timer, IConsole console) { m_Timer = timer; m_Timer.Enabled = true; m_Timer.Interval = 1000; m_Timer.TimerIntervalElapsed += Handler; m_Console = console; } public void StartPublishing() { m_Timer.Start(); } public void StopPublishing() { m_Timer.Stop(); } private void Handler(object sender, DateTime dateTime) { m_Console.WriteLine(dateTime); } } }


Bis auf kleine Änderungen ist es fast identisch mit dem alten Publisher . Jetzt haben wir den ITimer als Abhängigkeit definiert, die über den Konstruktor eingefügt wird. Der Rest des Codes wäre derselbe.

Timer

 using System; using System.Collections.Generic; using System.Linq; using System.Timers; using BetterTimerApp.Abstractions; namespace BetterTimerApp.Implementations { public class Timer : ITimer { private Dictionary<TimerIntervalElapsedEventHandler, List<ElapsedEventHandler>> m_Handlers = new(); private bool m_IsDisposed; private System.Timers.Timer m_Timer; public Timer() { m_Timer = new System.Timers.Timer(); } public event TimerIntervalElapsedEventHandler TimerIntervalElapsed { add { var internalHandler = (ElapsedEventHandler)((sender, args) => { value.Invoke(sender, args.SignalTime); }); if (!m_Handlers.ContainsKey(value)) { m_Handlers.Add(value, new List<ElapsedEventHandler>()); } m_Handlers[value].Add(internalHandler); m_Timer.Elapsed += internalHandler; } remove { m_Timer.Elapsed -= m_Handlers[value].Last(); m_Handlers[value].RemoveAt(m_Handlers[value].Count - 1); if (!m_Handlers[value].Any()) { m_Handlers.Remove(value); } } } public bool Enabled { get => m_Timer.Enabled; set => m_Timer.Enabled = value; } public double Interval { get => m_Timer.Interval; set => m_Timer.Interval = value; } public void Start() { m_Timer.Start(); } public void Stop() { m_Timer.Stop(); } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (m_IsDisposed) return; if (disposing && m_Handlers.Any()) { foreach (var internalHandlers in m_Handlers.Values) { if (internalHandlers?.Any() ?? false) { internalHandlers.ForEach(handler => m_Timer.Elapsed -= handler); } } m_Timer.Dispose(); m_Timer = null; m_Handlers.Clear(); m_Handlers = null; } m_IsDisposed = true; } ~Timer() { Dispose(false); } } }


Hier passiert fast die ganze Magie.


Was wir hier bemerken können:


  1. Intern verwenden wir System.Timers.Timer .


  2. Wir haben das IDisposable- Designmuster angewendet. Aus diesem Grund können Sie den private bool m_IsDisposed , public void Dispose() , protected virtual void Dispose(bool disposing) und den ~Timer() sehen.


  3. Im Konstruktor initialisieren wir eine neue Instanz von System.Timers.Timer . Wir würden dies in den restlichen Schritten als internen Timer bezeichnen.


  4. Für public bool Enabled , public double Interval , public void Start() und public void Stop() delegieren wir lediglich die Implementierung an den internen Timer.


  5. Für public event TimerIntervalElapsedEventHandler TimerIntervalElapsed ist dies der wichtigste Teil; Lassen Sie es uns also Schritt für Schritt analysieren.


  6. Was wir mit diesem Ereignis tun müssen, ist zu handhaben, wenn jemand es von außerhalb abonniert/abbestellt. In diesem Fall möchten wir dies auf den internen Timer spiegeln.


  7. Mit anderen Worten: Wenn jemand von außerhalb eine Instanz unseres ITimer hat, sollte er in der Lage sein, so etwas zu tun t.TimerIntervalElapsed += (sender, dateTime) => { //do something } .


  8. Was wir in diesem Moment tun sollten, ist intern etwas wie m_Timer.Elapsed += (sender, elapsedEventArgs) => { //do something } .


  9. Wir müssen jedoch bedenken, dass die beiden Handler nicht identisch sind, da sie tatsächlich unterschiedlichen Typs sind; TimerIntervalElapsedEventHandler und ElapsedEventHandler .


  10. Daher müssen wir den kommenden TimerIntervalElapsedEventHandler in einen neuen internen ElapsedEventHandler einschließen. Das können wir tun.


  11. Allerdings müssen wir auch bedenken, dass irgendwann jemand einen Handler vom TimerIntervalElapsedEventHandler -Ereignis abmelden muss.


  12. Das bedeutet, dass wir in diesem Moment wissen müssen, welcher ElapsedEventHandler Handler diesem TimerIntervalElapsedEventHandler Handler entspricht, damit wir ihn vom internen Timer abmelden können.


  13. Die einzige Möglichkeit, dies zu erreichen, besteht darin, jeden TimerIntervalElapsedEventHandler -Handler und den neu erstellten ElapsedEventHandler Handler in einem Wörterbuch zu verfolgen. Auf diese Weise können wir durch Kenntnis des übergebenen TimerIntervalElapsedEventHandler Handlers den entsprechenden ElapsedEventHandler Handler kennen.


  14. Wir müssen jedoch auch bedenken, dass jemand von außen möglicherweise denselben TimerIntervalElapsedEventHandler Handler mehr als einmal abonniert.


  15. Ja, das ist nicht logisch, aber dennoch machbar. Der Vollständigkeit halber würden wir daher für jeden TimerIntervalElapsedEventHandler Handler eine Liste der ElapsedEventHandler Handler führen.


  16. In den meisten Fällen enthält diese Liste nur einen Eintrag, es sei denn, es handelt sich um ein doppeltes Abonnement.


  17. Und deshalb können Sie dieses private Dictionary<TimerIntervalElapsedEventHandler, List<ElapsedEventHandler>> m_Handlers = new(); sehen. .


 public event TimerIntervalElapsedEventHandler TimerIntervalElapsed { add { var internalHandler = (ElapsedEventHandler)((sender, args) => { value.Invoke(sender, args.SignalTime); }); if (!m_Handlers.ContainsKey(value)) { m_Handlers.Add(value, new List<ElapsedEventHandler>()); } m_Handlers[value].Add(internalHandler); m_Timer.Elapsed += internalHandler; } remove { m_Timer.Elapsed -= m_Handlers[value].Last(); m_Handlers[value].RemoveAt(m_Handlers[value].Count - 1); if (!m_Handlers[value].Any()) { m_Handlers.Remove(value); } } }


Im add erstellen wir einen neuen ElapsedEventHandler , fügen einen Datensatz im Wörterbuch m_Handlers hinzu, der diesen TimerIntervalElapsedEventHandler zuordnet, und abonnieren schließlich den internen Timer.


In „ remove rufen wir die entsprechende Liste der ElapsedEventHandler Handler ab, wählen den letzten Handler aus, melden ihn vom internen Timer ab, entfernen ihn aus der Liste und entfernen den gesamten Eintrag, wenn die Liste leer ist.


Erwähnenswert ist auch die Dispose Implementierung.


 protected virtual void Dispose(bool disposing) { if (m_IsDisposed) return; if (disposing && m_Handlers.Any()) { foreach (var internalHandlers in m_Handlers.Values) { if (internalHandlers?.Any() ?? false) { internalHandlers.ForEach(handler => m_Timer.Elapsed -= handler); } } m_Timer.Dispose(); m_Timer = null; m_Handlers.Clear(); m_Handlers = null; } m_IsDisposed = true; }


Wir melden alle verbleibenden Handler vom internen Timer ab, entsorgen den internen Timer und löschen das m_Handlers Wörterbuch.

Programm

 using BetterTimerApp.Abstractions; using BetterTimerApp.Implementations; namespace BetterTimerApp { public class Program { static void Main(string[] args) { var timer = new Timer(); IPublisher publisher = new Publisher(timer, new Implementations.Console()); publisher.StartPublishing(); System.Console.ReadLine(); publisher.StopPublishing(); timer.Dispose(); } } }


Hier tun wir noch nicht viel. Es ist fast das Gleiche wie die alte Lösung.


Wenn Sie dies ausführen, sollte dies etwa so aussehen:


Bild von Ahmed Tarek


Foto von Testalize.me auf Unsplash, angepasst von Ahmed Tarek

Zeit zum Testen, der Moment der Wahrheit

Jetzt haben wir unser endgültiges Design. Wir müssen jedoch sehen, ob dieses Design uns wirklich dabei helfen kann, unser Publisher Modul mit Unit-Tests abzudecken.


Der Aufbau der Lösung sollte wie folgt aussehen:


Bild von Ahmed Tarek


Ich verwende NUnit und Moq zum Testen. Sie können sicher mit Ihren bevorzugten Bibliotheken arbeiten.

TimerStub

 using System; using System.Collections.Generic; using BetterTimerApp.Abstractions; namespace BetterTimerApp.Tests.Stubs { public enum Action { Start = 1, Stop = 2, Triggered = 3, Enabled = 4, Disabled = 5, IntervalSet = 6 } public class ActionLog { public Action Action { get; } public string Message { get; } public ActionLog(Action action, string message) { Action = action; Message = message; } } public class TimerStub : ITimer { private bool m_Enabled; private double m_Interval; public event TimerIntervalElapsedEventHandler TimerIntervalElapsed; public Dictionary<int, ActionLog> Log = new(); public bool Enabled { get => m_Enabled; set { m_Enabled = value; Log.Add(Log.Count + 1, new ActionLog(value ? Action.Enabled : Action.Disabled, value ? "Enabled" : "Disabled")); } } public double Interval { get => m_Interval; set { m_Interval = value; Log.Add(Log.Count + 1, new ActionLog(Action.IntervalSet, m_Interval.ToString("G17"))); } } public void Start() { Log.Add(Log.Count + 1, new ActionLog(Action.Start, "Started")); } public void Stop() { Log.Add(Log.Count + 1, new ActionLog(Action.Stop, "Stopped")); } public void TriggerTimerIntervalElapsed(DateTime dateTime) { OnTimerIntervalElapsed(dateTime); Log.Add(Log.Count + 1, new ActionLog(Action.Triggered, "Triggered")); } protected void OnTimerIntervalElapsed(DateTime dateTime) { TimerIntervalElapsed?.Invoke(this, dateTime); } public void Dispose() { Log.Clear(); Log = null; } } }


Was wir hier bemerken können:


  1. Wir haben die Action definiert, die beim Protokollieren der über unseren Timer-Stub durchgeführten Aktionen verwendet werden soll. Dies würde später verwendet werden, um die durchgeführten internen Aktionen geltend zu machen.


  2. Außerdem haben wir die ActionLog Klasse definiert, die für die Protokollierung verwendet werden soll.


  3. Wir haben die TimerStub Klasse als Stub von ITimer definiert. Wir würden diesen Stub später beim Testen des Publisher Moduls verwenden.


  4. Die Umsetzung ist einfach. Erwähnenswert ist, dass wir eine zusätzliche public void TriggerTimerIntervalElapsed(DateTime dateTime) hinzugefügt haben, damit wir den Stub innerhalb eines Komponententests manuell auslösen können.


  5. Wir können auch den erwarteten Wert von dateTime übergeben, sodass wir einen bekannten Wert haben, gegen den wir behaupten können.

PublisherTests

 using System; using BetterTimerApp.Abstractions; using BetterTimerApp.Implementations; using BetterTimerApp.Tests.Stubs; using Moq; using NUnit.Framework; using Action = BetterTimerApp.Tests.Stubs.Action; namespace BetterTimerApp.Tests.Tests { [TestFixture] public class PublisherTests { private TimerStub m_TimerStub; private Mock<IConsole> m_ConsoleMock; private Publisher m_Sut; [SetUp] public void SetUp() { m_TimerStub = new TimerStub(); m_ConsoleMock = new Mock<IConsole>(); m_Sut = new Publisher(m_TimerStub, m_ConsoleMock.Object); } [TearDown] public void TearDown() { m_Sut = null; m_ConsoleMock = null; m_TimerStub = null; } [Test] public void ConstructorTest() { Assert.AreEqual(Action.Enabled, m_TimerStub.Log[1].Action); Assert.AreEqual(Action.Enabled.ToString(), m_TimerStub.Log[1].Message); Assert.AreEqual(Action.IntervalSet, m_TimerStub.Log[2].Action); Assert.AreEqual(1000.ToString("G17"), m_TimerStub.Log[2].Message); } [Test] public void StartPublishingTest() { // Arrange var expectedDateTime = DateTime.Now; m_ConsoleMock .Setup ( m => m.WriteLine ( It.Is<DateTime>(p => p == expectedDateTime) ) ) .Verifiable(); // Act m_Sut.StartPublishing(); m_TimerStub.TriggerTimerIntervalElapsed(expectedDateTime); // Assert ConstructorTest(); m_ConsoleMock .Verify ( m => m.WriteLine(expectedDateTime) ); Assert.AreEqual(Action.Start, m_TimerStub.Log[3].Action); Assert.AreEqual("Started", m_TimerStub.Log[3].Message); Assert.AreEqual(Action.Triggered, m_TimerStub.Log[4].Action); Assert.AreEqual(Action.Triggered.ToString(), m_TimerStub.Log[4].Message); } [Test] public void StopPublishingTest() { // Act m_Sut.StopPublishing(); // Assert ConstructorTest(); Assert.AreEqual(Action.Stop, m_TimerStub.Log[3].Action); Assert.AreEqual("Stopped", m_TimerStub.Log[3].Message); } [Test] public void FullProcessTest() { // Arrange var expectedDateTime1 = DateTime.Now; var expectedDateTime2 = expectedDateTime1 + TimeSpan.FromSeconds(1); var expectedDateTime3 = expectedDateTime2 + TimeSpan.FromSeconds(1); var sequence = new MockSequence(); m_ConsoleMock .InSequence(sequence) .Setup ( m => m.WriteLine ( It.Is<DateTime>(p => p == expectedDateTime1) ) ) .Verifiable(); m_ConsoleMock .InSequence(sequence) .Setup ( m => m.WriteLine ( It.Is<DateTime>(p => p == expectedDateTime2) ) ) .Verifiable(); m_ConsoleMock .InSequence(sequence) .Setup ( m => m.WriteLine ( It.Is<DateTime>(p => p == expectedDateTime3) ) ) .Verifiable(); // Act m_Sut.StartPublishing(); m_TimerStub.TriggerTimerIntervalElapsed(expectedDateTime1); // Assert ConstructorTest(); m_ConsoleMock .Verify ( m => m.WriteLine(expectedDateTime1) ); Assert.AreEqual(Action.Start, m_TimerStub.Log[3].Action); Assert.AreEqual("Started", m_TimerStub.Log[3].Message); Assert.AreEqual(Action.Triggered, m_TimerStub.Log[4].Action); Assert.AreEqual(Action.Triggered.ToString(), m_TimerStub.Log[4].Message); // Act m_TimerStub.TriggerTimerIntervalElapsed(expectedDateTime2); // Assert m_ConsoleMock .Verify ( m => m.WriteLine(expectedDateTime2) ); Assert.AreEqual(Action.Triggered, m_TimerStub.Log[5].Action); Assert.AreEqual(Action.Triggered.ToString(), m_TimerStub.Log[5].Message); // Act m_Sut.StopPublishing(); // Assert Assert.AreEqual(Action.Stop, m_TimerStub.Log[6].Action); Assert.AreEqual("Stopped", m_TimerStub.Log[6].Message); } } }


Wie Sie sehen, haben wir jetzt die volle Kontrolle und können unser Publisher Modul problemlos mit Unit-Tests abdecken.


Wenn wir die Abdeckung berechnen, sollten wir Folgendes erhalten:


Bild von Ahmed Tarek


Wie Sie sehen, ist das Publisher Modul zu 100 % abgedeckt. Im Übrigen ist dies nicht Gegenstand dieses Artikels, aber Sie können es einfach behandeln, wenn Sie dem Ansatz im Artikel folgen So decken Sie die .NET C#-Konsolenanwendung vollständig mit Komponententests ab .


Foto von Jingda Chen auf Unsplash, angepasst von Ahmed Tarek

Letzte Worte

Du kannst es schaffen. Es geht lediglich darum, große Module in kleinere aufzuteilen, Ihre Abstraktionen zu definieren, bei kniffligen Teilen kreativ zu werden, und schon sind Sie fertig.


Wenn Sie sich weiterbilden möchten, können Sie sich meine anderen Artikel über einige Best Practices ansehen.


Ich hoffe, Sie fanden die Lektüre dieses Artikels genauso interessant wie ich das Schreiben.


Auch hier veröffentlicht