paint-brush
.NET C#에서 타이머를 사용하는 가장 좋은 방법~에 의해@ahmedtarekhasan
4,376 판독값
4,376 판독값

.NET C#에서 타이머를 사용하는 가장 좋은 방법

~에 의해 Ahmed Tarek Hasan30m2023/03/29
Read on Terminal Reader
Read this story w/o Javascript

너무 오래; 읽다

C# 애플리케이션에서 System.Timers.Timer를 사용하면 이를 추상화하고 단위 테스트로 모듈을 처리하는 데 문제가 발생할 수 있습니다. 이 기사에서는 이러한 과제를 극복하는 방법에 대한 모범 사례에 대해 논의하겠습니다. 결국에는 모듈의 100% 적용 범위를 달성할 수 있습니다.
featured image - .NET C#에서 타이머를 사용하는 가장 좋은 방법
Ahmed Tarek Hasan HackerNoon profile picture

타이머를 완전히 제어하고 단위 테스트를 통해 100% 적용 범위에 도달하는 방법

.NET C# 애플리케이션에서 System.Timers.Timer를 사용할 때 이를 추상화하고 단위 테스트로 모듈을 처리하는 데 문제가 발생할 수 있습니다.


이 기사에서는 이러한 과제를 해결하는 방법에 대한 모범 사례에 대해 논의하고 결국에는 모듈을 100% 적용할 수 있게 될 것입니다.


Unsplash에 Lina Trochez가 찍은 사진

접근

이것이 우리가 솔루션에 접근하는 방법입니다.


  1. 작업할 아주 간단한 예를 생각해 보세요.


  2. 간단하고 나쁜 해결책부터 시작하세요.


  3. 최종 형식에 도달할 때까지 계속해서 개선해 보세요.


  4. 여행을 통해 배운 교훈을 요약합니다.


Unsplash에 James Harrison이 찍은 사진

이 예에서는 System.Timers.Timer 사용하여 매초마다 날짜와 시간을 콘솔에 쓰는 간단한 작업만 수행하는 간단한 콘솔 응용 프로그램을 구축합니다.


결국에는 다음과 같이 끝나야 합니다.


이미지 제공: Ahmed Tarek


보시다시피 요구 사항 측면에서는 간단하고 화려하지 않습니다.


Unsplash의 Mikael Seegen 사진, Ahmed Tarek 조정

부인 성명

  1. 이 문서에서 대상으로 삼은 다른 모범 사례에 중점을 두기 위해 일부 모범 사례는 무시되거나 삭제될 수 있습니다.


  2. 이 기사에서는 단위 테스트와 함께 System.Timers.Timer를 사용하는 모듈을 다루는 데 중점을 둘 것입니다. 그러나 솔루션의 나머지 부분은 단위 테스트에서 다루지 않습니다. 이에 대해 더 알고 싶으시다면 기사를 확인해보세요 단위 테스트로 .NET C# 콘솔 애플리케이션을 완벽하게 다루는 방법 .


  3. 거의 유사한 결과를 얻는 데 사용할 수 있는 일부 타사 라이브러리가 있습니다. 그러나 가능할 때마다 대규모 타사 라이브러리 전체에 의존하기보다는 기본적으로 단순한 디자인을 따르는 것이 좋습니다.


Unsplash의 Maria Teneva 사진, Ahmed Tarek 조정

나쁜 해결책

이 솔루션에서는 추상화 계층을 제공하지 않고 System.Timers.Timer를 직접 사용합니다.


솔루션의 구조는 다음과 같아야 합니다.


이미지 제공: Ahmed Tarek


Console TimerApp 프로젝트가 하나만 있는 UsingTimer 솔루션입니다.


나는 의도적으로 System.Console IConsole 로 추상화하는 데 약간의 시간과 노력을 투자하여 이것이 Timer 문제를 해결하지 못할 것임을 증명했습니다.


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


이 예에서는 System.Console.WriteLine 만 사용하면 됩니다. 이것이 유일한 추상화 방법인 이유입니다.


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


IPublisher 인터페이스에는 StartPublishingStopPublishing 이라는 두 가지 메서드만 있습니다.


이제 구현을 위해:


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


Console 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 IPublisher 의 간단한 구현입니다. System.Timers.Timer 를 사용하여 구성하고 있습니다.


종속성으로 정의된 IConsole 있습니다. 이것은 내 관점에서 볼 때 모범 사례가 아닙니다. 무슨 말인지 이해하고 싶다면 기사를 확인해 보세요. .NET C#에서 DI, IoC 및 IoC 컨테이너를 사용하지 말아야 하는 경우 .


그러나 단순화를 위해서만 생성자에 종속성으로 주입하겠습니다.


또한 타이머 간격을 1000밀리초(1초)로 설정하고 타이머 SignalTime 콘솔에 기록하도록 핸들러를 설정합니다.


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


여기 Program 클래스에서는 많은 작업을 수행하지 않습니다. 이제 Publisher 클래스의 인스턴스를 만들고 게시를 시작하는 중입니다.


이것을 실행하면 다음과 같이 끝날 것입니다.


이미지 제공: Ahmed Tarek


이제 문제는 Publisher 클래스에 대한 단위 테스트를 작성하려면 무엇을 할 수 있느냐는 것입니다.


불행히도 대답은 다음과 같습니다. 너무 많지는 않습니다 .


첫째, 타이머 자체를 종속성으로 주입하지 않습니다. 이는 Publisher 클래스 내부에 종속성을 숨기고 있음을 의미합니다. 따라서 타이머를 조롱하거나 스텁할 수 없습니다.


둘째, 이제 Timer가 생성자에 주입되도록 코드를 수정했다고 가정해 보겠습니다. 그래도 문제는 어떻게 단위 테스트를 작성하고 타이머를 모의 또는 스텁으로 대체할 것인가 하는 것입니다.


누군가 "타이머를 추상화하여 타이머 대신 주입하자"고 외치는 소리가 들립니다.


예, 맞습니다. 하지만 그렇게 간단하지는 않습니다. 다음 섹션에서 설명할 몇 가지 트릭이 있습니다.


Unsplash의 Carson Masterson 사진, Ahmed Tarek 조정

좋은 솔루션

지금은 좋은 해결책이 필요한 때입니다. 이에 대해 우리가 무엇을 할 수 있는지 살펴보겠습니다.


솔루션의 구조는 다음과 같아야 합니다.


이미지 제공: Ahmed Tarek


새로운 Console BetterTimerApp 프로젝트와 동일한 UsingTimer 솔루션입니다.


IConsole , IPublisherConsole 동일합니다.

아이타이머

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


여기서 우리가 알 수 있는 것은:


  1. 새로운 대리자 TimerIntervalElapsedEventHandler 정의했습니다. 이 대리자는 ITimer 에서 발생하는 이벤트를 나타냅니다.


  2. 이미 System.Timers.Timer 에서 사용하는 기본 ElapsedEventHandler 있으므로 이 새 대리자가 필요하지 않다고 주장할 수도 있습니다.


  3. 그래 이건 사실이야. 그러나 ElapsedEventHandler 이벤트는 ElapsedEventArgs 이벤트 인수로 제공하고 있음을 알 수 있습니다. 이 ElapsedEventArgs 에는 전용 생성자가 있으므로 고유한 인스턴스를 만들 수 없습니다. 또한 ElapsedEventArgs 클래스에 정의된 SignalTime 속성은 읽기 전용입니다. 따라서 하위 클래스에서는 이를 재정의할 수 없습니다.


  4. 이 클래스를 업데이트하기 위해 Microsoft에 변경 요청 티켓이 열려 있지만 이 기사를 작성하는 순간까지는 변경 사항이 적용되지 않았습니다.


  5. 또한 ITimer IDisposable 을 확장합니다.



발행자

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


작은 변화를 제외하면 기존 Publisher 와 거의 동일합니다. 이제 생성자를 통해 주입되는 종속성으로 정의된 ITimer 있습니다. 나머지 코드는 동일합니다.

시간제 노동자

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


거의 모든 마법이 일어나는 곳입니다.


여기서 우리가 알 수 있는 것은:


  1. 내부적으로는 System.Timers.Timer 사용하고 있습니다.


  2. IDisposable 디자인 패턴을 적용했습니다. 이것이 바로 private bool m_IsDisposed , public void Dispose() , protected virtual void Dispose(bool disposing)~Timer() 볼 수 있는 이유입니다.


  3. 생성자에서는 System.Timers.Timer 의 새 인스턴스를 초기화합니다. 나머지 단계에서는 이를 내부 타이머 라고 부릅니다.


  4. public bool Enabled , public double Interval , public void Start()public void Stop() 의 경우 구현을 내부 타이머에 위임합니다.


  5. public event TimerIntervalElapsedEventHandler TimerIntervalElapsed 의 경우 이것이 가장 중요한 부분입니다. 그럼 단계별로 분석해 보겠습니다.


  6. 이 이벤트와 관련하여 우리가 해야 할 일은 누군가가 외부에서 구독/구독 취소할 때를 처리하는 것입니다. 이 경우에는 이를 내부 타이머에 미러링하려고 합니다.


  7. 즉, 외부의 누군가가 우리 ITimer 인스턴스를 갖고 있다면 그는 다음과 같은 작업을 수행할 수 있어야 합니다 t.TimerIntervalElapsed += (sender, dateTime) => { //do something } .


  8. 이 순간 우리가 해야 할 일은 내부적으로 m_Timer.Elapsed += (sender, elapsedEventArgs) => { //do something } 작업을 수행하는 것입니다.


  9. 그러나 두 핸들러는 실제로는 다른 유형이므로 동일하지 않다는 점을 명심해야 합니다. TimerIntervalElapsedEventHandlerElapsedEventHandler .


  10. 따라서 우리가 해야 할 일은 TimerIntervalElapsedEventHandler 에서 들어오는 것을 새로운 내부 ElapsedEventHandler 로 래핑하는 것입니다. 이것은 우리가 할 수 있는 일입니다.


  11. 그러나 어떤 시점에서는 누군가가 TimerIntervalElapsedEventHandler 이벤트에서 핸들러 구독을 취소해야 할 수도 있다는 점도 염두에 두어야 합니다.


  12. 이는 현재 내부 타이머에서 구독을 취소할 수 있도록 해당 TimerIntervalElapsedEventHandler 핸들러에 해당하는 ElapsedEventHandler 핸들러를 알 수 있어야 함을 의미합니다.


  13. 이를 달성하는 유일한 방법은 각 TimerIntervalElapsedEventHandler 핸들러와 새로 생성된 ElapsedEventHandler 핸들러를 사전에 추적하는 것입니다. 이런 식으로 전달된 TimerIntervalElapsedEventHandler 핸들러를 알면 해당 ElapsedEventHandler 핸들러를 알 수 있습니다.


  14. 그러나 외부에서 누군가가 동일한 TimerIntervalElapsedEventHandler 핸들러를 두 번 이상 구독할 수도 있다는 점도 명심해야 합니다.


  15. 예, 이것은 논리적이지 않지만 여전히 가능합니다. 따라서 완전성을 위해 각 TimerIntervalElapsedEventHandler 핸들러에 대해 ElapsedEventHandler 핸들러 목록을 유지합니다.


  16. 대부분의 경우 중복 구독이 아닌 이상 이 목록에는 항목이 하나만 있습니다.


  17. 이것이 바로 이 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 에서는 새로운 ElapsedEventHandler 생성하고 이를 TimerIntervalElapsedEventHandler 에 매핑하는 사전인 m_Handlers 에 레코드를 추가하고 마지막으로 내부 타이머를 구독합니다.


remove 에서는 ElapsedEventHandler 핸들러의 해당 목록을 가져오고, 마지막 핸들러를 선택하고, 내부 타이머에서 이를 구독 취소하고, 목록에서 제거하고, 목록이 비어 있으면 전체 항목을 제거합니다.


또한 언급할 가치가 있는 것은 Dispose 구현입니다.


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


내부 타이머에서 나머지 모든 핸들러를 구독 취소하고, 내부 타이머를 삭제하고, m_Handlers 사전을 지웁니다.

프로그램

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


여기서 우리는 아직 많은 일을 하고 있지 않습니다. 이전 솔루션과 거의 동일합니다.


이것을 실행하면 다음과 같이 끝날 것입니다.


이미지 제공: Ahmed Tarek


Unsplash의 Testalize.me 사진, Ahmed Tarek 조정

시험의 시간, 진실의 순간

이제 최종 디자인이 완성되었습니다. 그러나 이 디자인이 Publisher 모듈을 단위 테스트로 다루는 데 실제로 도움이 될 수 있는지 확인해야 합니다.


솔루션의 구조는 다음과 같아야 합니다.


이미지 제공: Ahmed Tarek


테스트를 위해 NUnitMoq를 사용하고 있습니다. 선호하는 라이브러리로 확실히 작업할 수 있습니다.

타이머스터브

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


여기서 우리가 알 수 있는 것은:


  1. Timer 스텁을 통해 수행된 작업을 기록하는 동안 사용할 Action 열거형을 정의했습니다. 이는 나중에 수행된 내부 작업을 확인하는 데 사용됩니다.


  2. 또한 로깅에 사용할 ActionLog 클래스를 정의했습니다.


  3. TimerStub 클래스를 ITimer 의 스텁으로 정의했습니다. 나중에 Publisher 모듈을 테스트할 때 이 스텁을 사용합니다.


  4. 구현은 간단합니다. 단위 테스트 내에서 스텁을 수동으로 트리거할 수 있도록 추가 public void TriggerTimerIntervalElapsed(DateTime dateTime) 메서드를 추가했다는 점을 언급할 가치가 있습니다.


  5. 또한 주장할 알려진 값을 갖도록 dateTime 의 예상 값을 전달할 수도 있습니다.

게시자 테스트

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


이제 보시다시피 우리는 모든 권한을 갖고 있으며 단위 테스트를 통해 Publisher 모듈을 쉽게 다룰 수 있습니다.


적용 범위를 계산하면 다음과 같은 결과를 얻을 수 있습니다.


이미지 제공: Ahmed Tarek


보시 Publisher 모듈은 100% 적용됩니다. 나머지 부분에 대해서는 이 기사의 범위를 벗어나지만 기사의 접근 방식을 따르면 간단히 다룰 수 있습니다. 단위 테스트로 .NET C# 콘솔 애플리케이션을 완벽하게 다루는 방법 .


Unsplash의 Jingda Chen 사진, Ahmed Tarek 조정

최종 단어

할 수 있어요. 큰 모듈을 작은 모듈로 나누고, 추상화를 정의하고, 까다로운 부분으로 창의력을 발휘하면 작업이 완료됩니다.


자신을 더 많이 훈련하고 싶다면 몇 가지 모범 사례에 대한 다른 기사를 확인하세요.


그게 다입니다. 이 기사를 읽는 것이 제가 쓴 것만큼 흥미로웠기를 바랍니다.


여기에도 게시됨