paint-brush
.NET C#'ta Zamanlayıcıları Kullanmanın En İyi Yoluile@ahmedtarekhasan
4,384 okumalar
4,384 okumalar

.NET C#'ta Zamanlayıcıları Kullanmanın En İyi Yolu

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

Çok uzun; Okumak

C# uygulamanızda System.Timers.Timer'ı kullandığınızda, onu soyutlama ve modüllerinizi Birim Testleri ile kapsayabilme konusunda sorunlarla karşılaşabilirsiniz. Bu makalede, bu zorlukların üstesinden nasıl gelinebileceğine dair en iyi uygulamaları tartışacağız. Sonunda modüllerinizin %100 kapsamını elde edebileceksiniz.
featured image - .NET C#'ta Zamanlayıcıları Kullanmanın En İyi Yolu
Ahmed Tarek Hasan HackerNoon profile picture

Zamanlayıcı Üzerinde Tam Kontrol Nasıl Sağlanır ve Birim Testleriyle %100 Kapsama Nasıl Ulaşılır?

.NET C# uygulamanızda System.Timers.Timer'ı kullanırken, onu soyutlama ve modüllerinizi Birim Testleri ile kapsayabilme konusunda sorunlarla karşılaşabilirsiniz.


Bu makalede, bu zorlukların üstesinden nasıl gelebileceğinize dair En İyi Uygulamaları tartışacağız ve sonunda modüllerinizin %100 kapsamını elde edebileceksiniz.


Unsplash'ta Lina Trochez'in fotoğrafı

Yaklaşım

Çözümümüze şu şekilde yaklaşacağız:


  1. Üzerinde çalışmak için çok basit bir örnek bulun.


  2. Basit kötü çözümle başlayın.


  3. Nihai formata ulaşana kadar onu geliştirmeye devam edin.


  4. Yolculuğumuz boyunca öğrenilen dersleri özetliyoruz.


Unsplash'ta James Harrison'ın fotoğrafı

Örnek

Örneğimizde, yalnızca tek bir basit şeyi yapacak basit bir Konsol Uygulaması oluşturacağız: System.Timers.Timer kullanarak konsola her saniye tarih ve saati yazın.


Sonunda şunu elde etmelisiniz:


Resim: Ahmed Tarek


Gördüğünüz gibi gereksinimler açısından basit, süslü bir şey değil.


Unsplash'ta Mikael Seegen'in fotoğrafı, Ahmed Tarek tarafından düzenlenmiştir

Sorumluluk reddi beyanı

  1. Bu makalede hedeflenen diğer en iyi uygulamalara odaklanılması amacıyla bazı en iyi uygulamalar göz ardı edilecek/bırakılacaktır.


  2. Bu yazıda System.Timers.Timer kullanan modülü birim testleriyle ele almaya odaklanacağız. Ancak çözümün geri kalanı birim testlerin kapsamına girmez. Bu konuda daha fazla bilgi edinmek istiyorsanız makaleye göz atabilirsiniz. .NET C# Konsol Uygulamasını Birim Testleriyle Tamamen Kapsama .


  3. Neredeyse benzer sonuçlara ulaşmak için kullanılabilecek bazı üçüncü taraf kütüphaneler vardır. Bununla birlikte, mümkün olduğunda, büyük bir üçüncü taraf kütüphanesine bağlı kalmaktansa, yerel, basit bir tasarımı takip etmeyi tercih ederim.


Unsplash'ta Maria Teneva'nın fotoğrafı, Ahmed Tarek tarafından düzenlenmiştir

Kötü Çözüm

Bu çözümde, bir soyutlama katmanı sağlamadan doğrudan System.Timers.Timer'ı kullanacağız.


Çözümün yapısı şöyle görünmelidir:


Resim: Ahmed Tarek


Yalnızca bir Konsol TimerApp projesine sahip bir TakingTimer çözümüdür.


Bunun Timer ile ilgili sorunumuzu çözmeyeceğini kanıtlamak için System.Console IConsole soyutlamak için kasıtlı olarak biraz zaman ve çaba harcadım.


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


Örneğimizde yalnızca System.Console.WriteLine kullanmamız gerekecek; bu yüzden soyutlanmış tek yöntem budur.


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


IPublisher arayüzünde yalnızca iki yöntemimiz var: StartPublishing ve StopPublishing .


Şimdi uygulamalara geçelim:


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


Console System.Console için yalnızca ince bir sarmalayıcıdır.


 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 IPublisher basit bir uygulamasıdır. Bir System.Timers.Timer kullanıyor ve sadece yapılandırıyor.


Bağımlılık olarak tanımlanan IConsole sahiptir. Bu benim açımdan en iyi uygulama değil. Ne demek istediğimi anlamak istiyorsanız makaleye göz atabilirsiniz. .NET C#'ta DI, IoC ve IoC Kapsayıcıları Ne Zaman Kullanılmamalıdır? .


Ancak basitlik adına bunu yapıcıya bir bağımlılık olarak enjekte edeceğiz.


Ayrıca Timer aralığını 1000 Milisaniye (1 Saniye) olarak ayarlıyoruz ve işleyiciyi Timer SignalTime değerini Konsola yazacak şekilde ayarlıyoruz.


 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(); } } }


Burada, Program dersinde pek bir şey yapmıyoruz. Sadece Publisher sınıfının bir örneğini oluşturuyoruz ve yayınlamaya başlıyoruz.


Bunu çalıştırmak şöyle bir şeyle sonuçlanmalı:


Resim: Ahmed Tarek


Şimdi soru şu; Publisher sınıfı için bir birim testi yazacaksanız ne yapabilirsiniz?


Ne yazık ki cevap şu olacaktır: çok fazla değil .


Öncelikle Timer'ın kendisini bir bağımlılık olarak enjekte etmiyorsunuz. Bu, bağımlılığı Publisher sınıfının içinde sakladığınız anlamına gelir. Bu nedenle Zamanlayıcıyla dalga geçemeyiz veya onu saptıramayız.


İkinci olarak, Timer'ın artık yapıcıya enjekte edilmesini sağlayacak şekilde kodu değiştirdiğimizi varsayalım; yine de soru, bir birim testinin nasıl yazılacağı ve Zamanlayıcının sahte veya saplamayla nasıl değiştirileceği olacaktır.


Birinin "Zamanlayıcıyı bir soyutlamaya saralım ve Zamanlayıcı yerine onu enjekte edelim" diye bağırdığını duyuyorum.


Evet doğru ama bu o kadar basit değil. Bir sonraki bölümde açıklayacağım bazı püf noktaları var.


Unsplash'ta Carson Masterson'ın fotoğrafı, Ahmed Tarek tarafından düzenlenmiştir

Güzel çözüm

İyi bir çözümün zamanı geldi. Bu konuda ne yapabileceğimize bir bakalım.


Çözümün yapısı şöyle görünmelidir:


Resim: Ahmed Tarek


Yeni bir Console BetterTimerApp projesiyle aynı TakingTimer çözümüdür.


IConsole , IPublisher ve Console aynı olacaktır.

IZamanlayıcı

 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(); } }


Burada neyi fark edebiliriz:


  1. Yeni temsilci TimerIntervalElapsedEventHandler tanımladık. Bu delege ITimer tarafından gündeme getirilecek olayı temsil ediyor.


  2. Zaten System.Timers.Timer tarafından kullanılan yerel ElapsedEventHandler sahip olduğumuz için bu yeni temsilciye ihtiyacımız olmadığını iddia edebilirsiniz.


  3. Evet bu doğru. Ancak ElapsedEventHandler olayının, olay bağımsız değişkenleri olarak ElapsedEventArgs sağladığını fark edeceksiniz. Bu ElapsedEventArgs özel bir yapıcısı vardır ve kendi örneğinizi oluşturamazsınız. Ayrıca ElapsedEventArgs sınıfında tanımlanan SignalTime özelliği salt okunurdur. Bu nedenle, bunu bir alt sınıfta geçersiz kılamazsınız.


  4. Microsoft'un bu sınıfı güncellemesi için açılmış bir değişiklik talebi bildirimi var ancak bu yazının yazıldığı ana kadar herhangi bir değişiklik uygulanmadı.


  5. Ayrıca ITimer IDisposable genişlettiğini unutmayın.



Yayımcı

 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); } } }


Küçük değişiklikler dışında eski Publisher ile neredeyse aynı. Artık yapıcı aracılığıyla enjekte edilen bir bağımlılık olarak tanımlanan ITimer var. Kodun geri kalanı aynı olacaktır.

Zamanlayıcı

 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); } } }


Burası neredeyse tüm sihrin gerçekleştiği yer.


Burada neyi fark edebiliriz:


  1. Dahili olarak System.Timers.Timer kullanıyoruz.


  2. IDisposable tasarım desenini uyguladık. Bu nedenle private bool m_IsDisposed , public void Dispose() , protected virtual void Dispose(bool disposing) ve ~Timer() görebilirsiniz.


  3. Yapıcıda, System.Timers.Timer yeni bir örneğini başlatıyoruz. Geri kalan adımlarda buna Dahili Zamanlayıcı adını vereceğiz.


  4. public bool Enabled , public double Interval , public void Start() ve public void Stop() için uygulamayı yalnızca Dahili Zamanlayıcıya devrediyoruz.


  5. public event TimerIntervalElapsedEventHandler TimerIntervalElapsed için bu en önemli kısımdır; o halde hadi adım adım analiz edelim.


  6. Bu etkinlikle ilgili yapmamız gereken, birisinin dışarıdan abone olmasını/abonelikten çıkmasını ele almaktır. Bu durumda bunu Dahili Zamanlayıcıya yansıtmak istiyoruz.


  7. Başka bir deyişle, dışarıdan biri ITimer örneğinize sahipse, bunun gibi bir şey yapabilmesi gerekir t.TimerIntervalElapsed += (sender, dateTime) => { //do something } .


  8. Şu anda yapmamız gereken dahili olarak m_Timer.Elapsed += (sender, elapsedEventArgs) => { //do something } gibi bir şey yapmaktır.


  9. Ancak iki işleyicinin aynı olmadığını, aslında farklı türde olduklarını aklımızda tutmamız gerekiyor; TimerIntervalElapsedEventHandler ve ElapsedEventHandler .


  10. Bu nedenle yapmamız gereken, TimerIntervalElapsedEventHandler içindeki gelenleri yeni bir dahili ElapsedEventHandler içine sarmaktır. Bu yapabileceğimiz bir şey.


  11. Ancak, bir noktada birisinin bir işleyicinin TimerIntervalElapsedEventHandler olayına olan aboneliğini iptal etmesi gerekebileceğini de aklımızda tutmamız gerekir.


  12. Bu, şu anda hangi ElapsedEventHandler işleyicisinin o TimerIntervalElapsedEventHandler işleyicisine karşılık geldiğini bilmemiz gerektiği anlamına gelir, böylece onu Dahili Zamanlayıcı aboneliğinden çıkarabiliriz.


  13. Bunu başarmanın tek yolu, her TimerIntervalElapsedEventHandler işleyicisini ve yeni oluşturulan ElapsedEventHandler işleyicisini bir sözlükte takip etmektir. Bu şekilde, TimerIntervalElapsedEventHandler işleyicisinde aktarılanları bilerek karşılık gelen ElapsedEventHandler işleyicisini bilebiliriz.


  14. Ancak dışarıdan birisinin aynı TimerIntervalElapsedEventHandler işleyicisine birden fazla kez abone olabileceğini de unutmamalıyız.


  15. Evet, bu mantıklı değil ama yine de yapılabilir. Bu nedenle, bütünlüğün sağlanması adına, her TimerIntervalElapsedEventHandler işleyicisi için, ElapsedEventHandler işleyicilerinin bir listesini tutacağız.


  16. Çoğu durumda, yinelenen bir abonelik olmadığı sürece bu listede yalnızca bir giriş bulunur.


  17. İşte bu yüzden bu private Dictionary<TimerIntervalElapsedEventHandler, List<ElapsedEventHandler>> m_Handlers = new(); .


 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); } } }


add yeni bir ElapsedEventHandler oluşturuyoruz, m_Handlers bunu TimerIntervalElapsedEventHandler ile eşleyen sözlüğe bir kayıt ekliyoruz ve son olarak Internal Timer'a abone oluyoruz.


remove dosyasında, ElapsedEventHandler işleyicilerinin ilgili listesini alıyoruz, son işleyiciyi seçiyoruz, Dahili Zamanlayıcı aboneliğinden çıkıyoruz, onu listeden kaldırıyoruz ve liste boşsa tüm girişi kaldırıyoruz.


Ayrıca Dispose uygulamasından da bahsetmeye değer.


 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; }


Geriye kalan tüm işleyicilerin Dahili Zamanlayıcı aboneliğinden çıkıyoruz, Dahili Zamanlayıcıyı atıyoruz ve m_Handlers sözlüğünü temizliyoruz.

programı

 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(); } } }


Burada hâlâ pek bir şey yapmıyoruz. Eski çözümle neredeyse aynı.


Bunu çalıştırmak şöyle bir şeyle sonuçlanmalı:


Resim: Ahmed Tarek


Unsplash'ta Testalize.me tarafından çekilmiş fotoğraf, Ahmed Tarek tarafından düzenlenmiştir

Test Zamanı, Gerçeğin Anı

Artık son tasarımımız var. Ancak bu tasarımın gerçekten Publisher modülümüzü birim testleriyle ele almamıza yardımcı olup olamayacağını görmemiz gerekiyor.


Çözümün yapısı şöyle görünmelidir:


Resim: Ahmed Tarek


Test için NUnit ve Moq kullanıyorum. Kesinlikle tercih ettiğiniz kütüphanelerle çalışabilirsiniz.

Zamanlayıcı Saplaması

 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; } } }


Burada neyi fark edebiliriz:


  1. Zamanlayıcı saplamamız aracılığıyla gerçekleştirilen eylemleri günlüğe kaydederken kullanılacak Action numaralandırmasını tanımladık. Bu daha sonra gerçekleştirilen dahili eylemleri onaylamak için kullanılacaktır.


  2. Ayrıca loglama için kullanılacak ActionLog sınıfını da tanımladık.


  3. TimerStub sınıfını ITimer bir saplaması olarak tanımladık. Bu saplamayı daha sonra Publisher modülünü test ederken kullanacağız.


  4. Uygulama basittir. Bir birim testi içinde saplamayı manuel olarak tetikleyebilmemiz için ekstra bir public void TriggerTimerIntervalElapsed(DateTime dateTime) yöntemi eklediğimizi belirtmekte fayda var.


  5. Ayrıca iddia edebileceğimiz bilinen bir değere sahip olmak için dateTime beklenen değerini de iletebiliriz.

YayıncıTestleri

 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); } } }


Artık gördüğünüz gibi tam kontrol bizde ve Publisher modülümüzü birim testlerle kolayca kapsayabiliyoruz.


Kapsamı hesaplarsak şunu elde etmeliyiz:


Resim: Ahmed Tarek


Gördüğünüz gibi Publisher modülü %100 kaplıdır. Geri kalanı için bu, bu makalenin kapsamı dışındadır, ancak makaledeki yaklaşımı izlerseniz bunu kolayca ele alabilirsiniz. .NET C# Konsol Uygulamasını Birim Testleriyle Tamamen Kapsama .


Unsplash'ta Jingda Chen'in fotoğrafı, Ahmed Tarek tarafından düzenlenmiştir

Son sözler

Bunu yapabilirsin. Bu sadece büyük modülleri daha küçük modüllere bölmek, soyutlamalarınızı tanımlamak, zor parçalarla yaratıcı olmak meselesidir ve sonra işiniz biter.


Kendinizi daha fazla eğitmek istiyorsanız bazı En İyi Uygulamalar hakkındaki diğer yazılarıma göz atabilirsiniz.


İşte bu kadar, umarım bu makaleyi okurken benim yazarken bulduğum kadar ilginç bulmuşsunuzdur.


Burada da yayınlandı