Làm thế nào để có toàn quyền kiểm soát bộ đếm thời gian và có thể đạt được mức độ phù hợp 100% với các bài kiểm tra đơn vị Khi sử dụng trong ứng dụng của bạn, bạn có thể gặp sự cố khi trừu tượng hóa nó và có thể bao gồm các mô-đun của mình bằng Bài kiểm tra đơn vị. System.Timers.Timer .NET C# Trong bài viết này, chúng ta sẽ thảo luận về về cách chinh phục những thách thức này và cuối cùng, bạn sẽ có thể đạt được mức độ phù hợp 100% cho các mô-đun của mình. Các phương pháp hay nhất Tiếp cận Đây là cách chúng ta sẽ tiếp cận giải pháp của mình: Hãy đến với một ví dụ rất đơn giản để làm việc trên. Bắt đầu với giải pháp xấu đơn giản. Tiếp tục cố gắng nâng cao nó cho đến khi chúng tôi đạt được định dạng cuối cùng. Tổng kết những bài học kinh nghiệm qua hành trình của chúng tôi. ví dụ Trong ví dụ của chúng tôi, chúng tôi sẽ xây dựng một đơn giản chỉ thực hiện một việc đơn giản: để ghi vào bảng điều khiển . Ứng dụng bảng điều khiển sử dụng System.Timers.Timer ngày và giờ mỗi giây Cuối cùng, bạn nên kết thúc với điều này: Như bạn có thể thấy, nó đơn giản về mặt yêu cầu, không có gì lạ mắt. từ chối trách nhiệm Một số phương pháp hay nhất sẽ bị bỏ qua/bỏ qua để hướng trọng tâm chính đến các phương pháp hay nhất khác được nhắm mục tiêu trong bài viết này. Trong bài viết này, chúng tôi sẽ tập trung vào việc trình bày mô-đun bằng cách sử dụng với các bài kiểm tra đơn vị. Tuy nhiên, phần còn lại của giải pháp sẽ không nằm trong các bài kiểm tra đơn vị. Nếu bạn muốn biết thêm về điều này, bạn có thể kiểm tra bài viết . System.Timers.Timer Cách bao quát toàn bộ ứng dụng bảng điều khiển .NET C# với các bài kiểm tra đơn vị Có một số thư viện của bên thứ ba có thể được sử dụng để đạt được kết quả gần như tương tự. Tuy nhiên, bất cứ khi nào có thể, tôi thà làm theo một thiết kế đơn giản gốc hơn là phụ thuộc vào toàn bộ thư viện lớn của bên thứ ba. Giải pháp tồi Trong giải pháp này, chúng tôi sẽ trực tiếp sử dụng mà không cung cấp lớp trừu tượng. System.Timers.Timer Cấu trúc của giải pháp sẽ trông như thế này: Đó là một giải pháp chỉ với một dự án . Sử dụng Timer Console TimerApp Tôi đã cố ý đầu tư thời gian và công sức vào việc trừu tượng hóa thành để chứng minh rằng điều này sẽ không giải quyết được vấn đề của chúng tôi với Bộ hẹn giờ. System.Console IConsole namespace TimerApp.Abstractions { public interface IConsole { void WriteLine(object? value); } } Chúng tôi chỉ cần sử dụng trong ví dụ của mình; đó là lý do tại sao đây là phương pháp trừu tượng duy nhất. System.Console.WriteLine namespace TimerApp.Abstractions { public interface IPublisher { void StartPublishing(); void StopPublishing(); } } Chúng tôi chỉ có hai phương thức trên giao diện : và . IPublisher StartPublishing StopPublishing Bây giờ, để thực hiện: using TimerApp.Abstractions; namespace TimerApp.Implementations { public class Console : IConsole { public void WriteLine(object? value) { System.Console.WriteLine(value); } } } chỉ là một trình bao bọc mỏng cho . 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); } } } là một triển khai đơn giản của . Nó đang sử dụng và chỉ cần cấu hình nó. Publisher IPublisher System.Timers.Timer Nó có được định nghĩa là một phần phụ thuộc. Đây không phải là một thực hành tốt nhất theo quan điểm của tôi. Nếu bạn muốn hiểu ý tôi, bạn có thể kiểm tra bài viết . IConsole Khi nào không sử dụng Bộ chứa DI, IoC và IoC trong .NET C# Tuy nhiên, chỉ vì mục đích đơn giản, chúng tôi sẽ chỉ thêm nó như một phần phụ thuộc vào hàm tạo. Chúng tôi cũng đang đặt khoảng thời gian của Bộ hẹn giờ thành 1000 mili giây (1 giây) và đặt trình xử lý để ghi của bộ hẹn giờ vào Bảng điều khiển. 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(); } } } Ở đây, trong lớp , chúng tôi không làm gì nhiều. Chúng ta chỉ đang tạo một thể hiện của lớp và bắt đầu xuất bản. Program Publisher Chạy này sẽ kết thúc với một cái gì đó như thế này: Bây giờ, câu hỏi là, nếu bạn định viết một bài kiểm tra đơn vị cho lớp , bạn có thể làm gì? Publisher Thật không may, câu trả lời sẽ là: . không quá nhiều Đầu tiên, bạn không tự tiêm Timer như một phần phụ thuộc. Điều này có nghĩa là bạn đang ẩn phần phụ thuộc bên trong lớp . Do đó, chúng tôi không thể chế nhạo hoặc khai thác Bộ hẹn giờ. Publisher Thứ hai, giả sử rằng chúng ta đã sửa đổi mã để Bộ hẹn giờ hiện được đưa vào hàm tạo; Tuy nhiên, câu hỏi sẽ là, làm thế nào để viết một bài kiểm tra đơn vị và thay thế Bộ hẹn giờ bằng một bản giả hoặc sơ khai? Tôi nghe thấy ai đó hét lên, "hãy gói Bộ hẹn giờ thành một khái niệm trừu tượng và đưa nó vào thay vì Bộ hẹn giờ." Vâng, đúng vậy, tuy nhiên, nó không đơn giản như vậy. Có một số thủ thuật mà tôi sẽ giải thích trong phần tiếp theo. Giải pháp tốt Đây là thời gian cho một giải pháp tốt. Hãy xem những gì chúng ta có thể làm về nó. Cấu trúc của giải pháp sẽ trông như thế này: Đó là cùng một giải pháp với dự án mới. Sử dụng Timer Console BetterTimerApp , và sẽ giống nhau. IConsole IPublisher Console 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(); } } Những gì chúng ta có thể nhận thấy ở đây: Chúng tôi đã xác định đại biểu mới . Đại biểu này đại diện cho sự kiện được đưa ra bởi của chúng tôi. TimerIntervalElapsedEventHandler ITimer Bạn có thể lập luận rằng chúng tôi không cần đại biểu mới này vì chúng tôi đã có gốc đã được sử dụng bởi . ElapsedEventHandler System.Timers.Timer Đúng vậy đây là sự thật. Tuy nhiên, bạn sẽ nhận thấy rằng sự kiện đang cung cấp làm đối số sự kiện. này có hàm tạo riêng và bạn sẽ không thể tạo phiên bản của riêng mình. Ngoài ra, thuộc tính được xác định trong lớp là chỉ đọc. Do đó, bạn sẽ không thể ghi đè nó trong một lớp con. ElapsedEventHandler ElapsedEventArgs ElapsedEventArgs SignalTime ElapsedEventArgs Có một vé yêu cầu thay đổi được mở để Microsoft cập nhật lớp này, nhưng cho đến thời điểm viết bài này, không có thay đổi nào được áp dụng. Ngoài ra, lưu ý rằng mở rộng . ITimer IDisposable nhà xuất bản 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); } } } Nó gần giống như cũ ngoại trừ những thay đổi nhỏ. Bây giờ, chúng ta có được định nghĩa là một phần phụ thuộc được đưa vào thông qua hàm tạo. Phần còn lại của mã sẽ giống nhau. Publisher ITimer hẹn giờ 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); } } } Đây là nơi gần như tất cả các phép thuật xảy ra. Những gì chúng ta có thể nhận thấy ở đây: Trong nội bộ, chúng tôi đang sử dụng . System.Timers.Timer Chúng tôi đã áp dụng mẫu thiết kế . Đó là lý do tại sao bạn có thể thấy , , và . IDisposable private bool m_IsDisposed public void Dispose() protected virtual void Dispose(bool disposing) ~Timer() Trong hàm tạo, chúng tôi đang khởi tạo một thể hiện mới của . Chúng tôi sẽ gọi đây là trong các bước còn lại. System.Timers.Timer Bộ hẹn giờ bên trong Đối với , , và , chúng ta chỉ ủy quyền triển khai cho Internal Timer. public bool Enabled public double Interval public void Start() public void Stop() Đối với , đây là phần quan trọng nhất; vì vậy hãy phân tích nó từng bước một. public event TimerIntervalElapsedEventHandler TimerIntervalElapsed Điều chúng ta cần làm với sự kiện này là xử lý khi ai đó đăng ký/hủy đăng ký từ bên ngoài. Trong trường hợp này, chúng tôi muốn phản chiếu điều này với Bộ hẹn giờ bên trong. Nói cách khác, nếu ai đó từ bên ngoài có phiên bản của chúng tôi, anh ta sẽ có thể làm điều gì đó như thế này . ITimer t.TimerIntervalElapsed += (sender, dateTime) => { //do something } Tại thời điểm này, điều chúng ta nên làm là thực hiện nội bộ một số thứ như . m_Timer.Elapsed += (sender, elapsedEventArgs) => { //do something } Tuy nhiên, chúng ta cần lưu ý rằng hai trình xử lý không giống nhau vì chúng thực sự thuộc các loại khác nhau; và . TimerIntervalElapsedEventHandler ElapsedEventHandler Do đó, những gì chúng ta cần làm là gói phần sắp tới trong vào một bên trong mới. Đây là điều chúng ta có thể làm. TimerIntervalElapsedEventHandler ElapsedEventHandler Tuy nhiên, chúng ta cũng cần lưu ý rằng, tại một thời điểm nào đó, ai đó có thể cần hủy đăng ký trình xử lý khỏi sự kiện . TimerIntervalElapsedEventHandler Điều này có nghĩa là, tại thời điểm này, chúng tôi cần biết trình xử lý nào tương ứng với trình xử lý đó để chúng tôi có thể hủy đăng ký nó khỏi Bộ hẹn giờ nội bộ. ElapsedEventHandler TimerIntervalElapsedEventHandler Cách duy nhất để đạt được điều này là theo dõi từng trình xử lý và trình xử lý mới được tạo trong từ điển. Bằng cách này, bằng cách biết trình xử lý được truyền trong , chúng ta có thể biết trình xử lý tương ứng. TimerIntervalElapsedEventHandler ElapsedEventHandler TimerIntervalElapsedEventHandler ElapsedEventHandler Tuy nhiên, chúng ta cũng cần lưu ý rằng từ bên ngoài, ai đó có thể đăng ký cùng một trình xử lý nhiều lần. TimerIntervalElapsedEventHandler Vâng, điều này không hợp lý, nhưng vẫn có thể thực hiện được. Do đó, để hoàn thiện, đối với mỗi trình xử lý , chúng tôi sẽ giữ một danh sách các trình xử lý . TimerIntervalElapsedEventHandler ElapsedEventHandler Trong hầu hết các trường hợp, danh sách này sẽ chỉ có một mục nhập trừ trường hợp đăng ký trùng lặp. Và đó là lý do tại sao bạn có thể thấy . 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); } } } Trong , chúng ta đang tạo một mới, thêm một bản ghi trong từ điển ánh xạ bản ghi này tới và cuối cùng đăng ký Internal Timer. add ElapsedEventHandler m_Handlers TimerIntervalElapsedEventHandler Trong , chúng tôi sẽ nhận được danh sách tương ứng của trình xử lý , chọn trình xử lý cuối cùng, hủy đăng ký nó khỏi Bộ hẹn giờ nội bộ, xóa nó khỏi danh sách và xóa toàn bộ mục nhập nếu danh sách trống. remove ElapsedEventHandler Cũng đáng nói, việc thực hiện . 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; } Chúng tôi đang hủy đăng ký tất cả các trình xử lý còn lại khỏi Bộ hẹn giờ bên trong, loại bỏ Bộ hẹn giờ bên trong và xóa từ điển . m_Handlers Chương trình 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(); } } } Ở đây, chúng tôi vẫn chưa làm được gì nhiều. Nó gần giống như giải pháp cũ. Chạy này sẽ kết thúc với một cái gì đó như thế này: Thời gian thử nghiệm, khoảnh khắc của sự thật Bây giờ, chúng tôi có thiết kế cuối cùng của chúng tôi. Tuy nhiên, chúng tôi cần xem liệu thiết kế này có thực sự có thể giúp chúng tôi bao quát mô-đun của mình bằng các bài kiểm tra đơn vị hay không. Publisher Cấu trúc của giải pháp sẽ trông như thế này: Tôi đang sử dụng và để thử nghiệm. Bạn chắc chắn có thể làm việc với các thư viện ưa thích của mình. NUnit Moq hẹn giờStub 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; } } } Những gì chúng ta có thể nhận thấy ở đây: Chúng tôi đã xác định enum sẽ được sử dụng trong khi ghi nhật ký các hành động được thực hiện thông qua sơ khai Bộ hẹn giờ của chúng tôi. Điều này sẽ được sử dụng sau này để xác nhận các hành động nội bộ được thực hiện. Action Ngoài ra, chúng tôi đã định nghĩa lớp sẽ được sử dụng để ghi nhật ký. ActionLog Chúng tôi đã định nghĩa lớp là sơ khai của . Chúng tôi sẽ sử dụng sơ khai này sau khi kiểm tra mô-đun . TimerStub ITimer Publisher Việc thực hiện rất đơn giản. Điều đáng nói là chúng tôi đã thêm một phương thức để chúng tôi có thể kích hoạt sơ khai theo cách thủ công trong một bài kiểm tra đơn vị. public void TriggerTimerIntervalElapsed(DateTime dateTime) Chúng tôi cũng có thể chuyển vào giá trị dự kiến của để chúng tôi có một giá trị đã biết để xác nhận. dateTime nhà xuất bản 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); } } } Bây giờ, như bạn có thể thấy, chúng tôi có toàn quyền kiểm soát và chúng tôi có thể dễ dàng bao quát mô-đun của mình bằng các bài kiểm tra đơn vị. Publisher Nếu chúng tôi tính toán phạm vi bảo hiểm, chúng tôi sẽ nhận được điều này: Như bạn có thể thấy, mô-đun được bao phủ 100%. Đối với phần còn lại, điều này nằm ngoài phạm vi của bài viết này, nhưng bạn có thể chỉ cần giải quyết nó nếu bạn làm theo cách tiếp cận trong bài viết . Publisher Cách bao quát toàn bộ ứng dụng bảng điều khiển .NET C# với các bài kiểm tra đơn vị Từ cuối cùng Bạn có thể làm được. Nó chỉ là vấn đề chia nhỏ các mô-đun lớn thành các mô-đun nhỏ hơn, xác định các phần trừu tượng của bạn, sáng tạo với các phần phức tạp, và sau đó bạn đã hoàn thành. Nếu bạn muốn rèn luyện bản thân nhiều hơn, bạn có thể xem các bài viết khác của tôi về một số Phương pháp hay nhất. Vậy đó, hy vọng bạn thấy thú vị khi đọc bài viết này như tôi thấy khi viết nó. Cũng được xuất bản ở đây