Phương pháp hay nhất để đạt được mức độ phù hợp 100% bằng cách sử dụng Phát triển theo hướng thử nghiệm (TDD), Tiêm phụ thuộc (DI), Đảo ngược kiểm soát (IoC) và Bộ chứa IoC.
Một số đồng nghiệp của tôi phàn nàn rằng đôi khi họ không thể áp dụng TDD hoặc viết bài kiểm tra đơn vị cho một số mô-đun hoặc ứng dụng, Ứng dụng bảng điều khiển là một trong số đó.
Làm cách nào tôi có thể kiểm tra ứng dụng Bảng điều khiển khi đầu vào được chuyển bằng tổ hợp phím và đầu ra được hiển thị trên màn hình?!!
Trên thực tế, điều này thỉnh thoảng xảy ra, bạn thấy mình đang cố gắng viết bài kiểm tra đơn vị cho thứ gì đó mà bạn dường như không có bất kỳ sự kiểm soát nào.
quan niệm sai lầm
Sự thật là, bạn vừa bỏ lỡ điểm. Bạn không cần kiểm tra ứng dụng “Bảng điều khiển”, bạn muốn kiểm tra logic nghiệp vụ đằng sau nó.
Khi bạn đang xây dựng ứng dụng Bảng điều khiển, bạn đang xây dựng ứng dụng cho ai đó sử dụng, anh ta mong muốn chuyển một số đầu vào và nhận được một số đầu ra tương ứng, và đó là điều bạn thực sự cần kiểm tra .
Bạn không muốn kiểm tra lớp tĩnh System.Console
, đây là lớp dựng sẵn được bao gồm trong .NET framework và bạn phải tin tưởng Microsoft về điều này.
Bây giờ, bạn cần suy nghĩ về cách tách hai khu vực này thành các thành phần hoặc mô-đun riêng biệt để bạn có thể bắt đầu viết bài kiểm tra cho phần mình muốn mà không can thiệp vào phần còn lại, và đây là điều tôi sẽ giải thích cho bạn…
Ý tưởng
Đầu tiên, hãy nghĩ ra một ý tưởng ứng dụng Console đơn giản ngu ngốc và sử dụng nó làm ví dụ để áp dụng.
Đầu tiên, bạn có menu đơn giản này.
Khi bạn chọn tùy chọn 1 và nhập tên của bạn , bạn sẽ nhận được thông báo Hello như trong hình bên dưới. Nhấn enter sẽ đóng ứng dụng.
Khi bạn chọn tùy chọn 2 và nhập tên của bạn , bạn sẽ nhận được thông báo Goodbye như trong hình bên dưới. Nhấn enter sẽ đóng ứng dụng.
Quá đơn giản đúng không? Vâng tôi đồng ý với bạn. Tuy nhiên, giả sử rằng giao diện người dùng, chuỗi, ký tự và mọi thứ bạn nhìn thấy trên màn hình đều là một phần của yêu cầu.
Điều này có nghĩa là nếu bạn định viết các bài kiểm tra đơn vị, thì điều này cũng phải được đề cập theo cách mà một thay đổi nhỏ trên một ký tự trong mã sản xuất sẽ kích hoạt một bài kiểm tra đơn vị không thành công.
Kế hoạch
Đây là kế hoạch của chúng tôi:
- Xây dựng ứng dụng Console theo cách xấu truyền thống.
- Xem liệu chúng ta có thể viết các bài kiểm tra đơn vị tự động hay không.
- Triển khai lại ứng dụng Console một cách tốt.
- Viết một số bài kiểm tra đơn vị.
con đường xấu
Đơn giản, làm mọi thứ ở một nơi.
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace MyConsoleApp { class Program { static void Main(string[] args) { var input = string.Empty; do { Console.WriteLine("Welcome to my console app"); Console.WriteLine("[1] Say Hello?"); Console.WriteLine("[2] Say Goodbye?"); Console.WriteLine(""); Console.Write("Please enter a valid choice: "); input = Console.ReadLine(); if (input == "1" || input == "2") { Console.Write("Please enter your name: "); string name = Console.ReadLine(); if (input == "1") { Console.WriteLine("Hello " + name); } else { Console.WriteLine("Goodbye " + name); } Console.WriteLine(""); Console.Write("Press any key to exit... "); Console.ReadKey(); } else { Console.Clear(); } } while (input != "1" && input != "2"); } } }
Những gì chúng ta có thể nhận thấy ở đây:
- Mọi thứ đều ở một nơi.
- Chúng tôi đang trực tiếp sử dụng lớp
System.Console
tĩnh. - Chúng tôi không thể kiểm tra logic nghiệp vụ mà không va vào
System.Console
.
Cố gắng viết bài kiểm tra đơn vị
Có thật không? bạn có thực sự mong đợi có thể viết một bài kiểm tra đơn vị cho mã đó không?
Dưới đây là những thách thức:
- Tùy thuộc vào các lớp tĩnh như
System.Console
. - Không thể xác định và cô lập các phụ thuộc.
- Không thể thay thế các phần phụ thuộc bằng Mocks hoặc Stub.
Nếu bạn có thể làm điều gì đó về nó, bạn là một anh hùng… tin tôi đi.
con đường tốt
Bây giờ, hãy chia giải pháp của chúng ta thành các mô-đun nhỏ hơn.
Trình quản lý bảng điều khiển
Đây là mô-đun chịu trách nhiệm cung cấp chức năng chúng tôi cần từ Bảng điều khiển… bất kỳ bảng điều khiển nào.
Mô-đun này sẽ bao gồm hai phần:
- trừu tượng.
- Triển khai.
Vì vậy, chúng tôi sẽ có những điều sau đây:
-
IConsoleManager
: Đây là giao diện xác định những gì chúng tôi đang mong đợi từ bất kỳ Trình quản lý bảng điều khiển nào. -
ConsoleManagerBase
: Đây là lớp trừu tượng triển khaiIConsoleManager
và cung cấp bất kỳ triển khai chung nào giữa tất cả Trình quản lý bảng điều khiển. -
ConsoleManager
: Đây là cài đặt Trình quản lý bảng điều khiển mặc định bao bọcSystem.Console
và thực sự được sử dụng trong thời gian chạy.
using System; namespace ConsoleManager { public interface IConsoleManager { void Write(string value); void WriteLine(string value); ConsoleKeyInfo ReadKey(); string ReadLine(); void Clear(); } }
using System; namespace ConsoleManager { public abstract class ConsoleManagerBase : IConsoleManager { public abstract void Clear(); public abstract ConsoleKeyInfo ReadKey(); public abstract string ReadLine(); public abstract void Write(string value); public abstract void WriteLine(string value); } }
using System; namespace ConsoleManager { public class ConsoleManager : ConsoleManagerBase { public override void Clear() { Console.Clear(); } public override ConsoleKeyInfo ReadKey() { return Console.ReadKey(); } public override string ReadLine() { return Console.ReadLine(); } public override void Write(string value) { Console.Write(value); } public override void WriteLine(string value) { Console.WriteLine(value); } } }
Những gì chúng ta có thể nhận thấy ở đây:
- Bây giờ chúng ta có
IConsoleManager
. - Chúng ta có thể sử dụng Mocks and Stub để thay thế
IConsoleManager
trong khi viết bài kiểm tra đơn vị. - Đối với lớp cơ sở chung
ConsoleManagerBase
chúng tôi không cung cấp bất kỳ triển khai chung nào cho trẻ em sử dụng. - Tôi biết đây không phải là điều tốt nhất để làm, tuy nhiên, tôi đang làm điều đó ở đây chỉ để nhắc bạn rằng tùy chọn này luôn sẵn có và bạn có thể sử dụng nó bất cứ khi nào cần.
Quản lý chương trình
Đây là mô-đun chịu trách nhiệm cung cấp chức năng ứng dụng chính.
Mô-đun này sẽ bao gồm hai phần:
- trừu tượng.
- Triển khai.
Vì vậy, chúng tôi sẽ có những điều sau đây:
-
IProgramManager
: Đây là giao diện xác định những gì chúng tôi đang mong đợi từ bất kỳ Trình quản lý chương trình nào. -
ProgramManagerBase
: Đây là lớp trừu tượng triển khaiIProgramManager
và cung cấp bất kỳ triển khai chung nào giữa tất cả các Trình quản lý chương trình. -
ProgramManager
quản lý chương trình : Đây là triển khai Trình quản lý chương trình mặc định được sử dụng thực sự trong thời gian chạy. Nó cũng phụ thuộc vàoIConsoleManager
.
namespace ProgramManager { public interface IProgramManager { void Run(); } }
namespace ProgramManager { public abstract class ProgramManagerBase : IProgramManager { public abstract void Run(); } }
using ConsoleManager; namespace ProgramManager { public class ProgramManager : ProgramManagerBase { private readonly IConsoleManager m_ConsoleManager; public ProgramManager(IConsoleManager consoleManager) { m_ConsoleManager = consoleManager; } public override void Run() { string input; do { m_ConsoleManager.WriteLine("Welcome to my console app"); m_ConsoleManager.WriteLine("[1] Say Hello?"); m_ConsoleManager.WriteLine("[2] Say Goodbye?"); m_ConsoleManager.WriteLine(""); m_ConsoleManager.Write("Please enter a valid choice: "); input = m_ConsoleManager.ReadLine(); if (input == "1" || input == "2") { m_ConsoleManager.Write("Please enter your name: "); var name = m_ConsoleManager.ReadLine(); if (input == "1") { m_ConsoleManager.WriteLine("Hello " + name); } else { m_ConsoleManager.WriteLine("Goodbye " + name); } m_ConsoleManager.WriteLine(""); m_ConsoleManager.Write("Press any key to exit... "); m_ConsoleManager.ReadKey(); } else { m_ConsoleManager.Clear(); } } while (input != "1" && input != "2" && input != "Exit"); } } }
Những gì chúng ta có thể nhận thấy ở đây:
- Bây giờ chúng ta đã xác định rõ sự phụ thuộc của Trình quản lý chương
ProgramManager
vàoIConsoleManager
. - Chúng tôi có
IProgramManager
và chúng tôi có thể sử dụng giả và sơ khai để thay thếIProgramManager
trong khi viết bài kiểm tra đơn vị. - Đối với lớp cơ sở chung
ProgramManagerBase
, chúng tôi không cung cấp bất kỳ triển khai chung nào cho trẻ em sử dụng. - Tôi biết đây không phải là điều tốt nhất để làm, tuy nhiên, tôi đang làm điều đó ở đây chỉ để nhắc bạn rằng tùy chọn này luôn sẵn có và bạn có thể sử dụng nó bất cứ khi nào cần.
Lớp ProgramManager
có thể được chia thành các phần nhỏ hơn. Điều đó sẽ giúp việc theo dõi và kiểm tra đơn vị trở nên dễ dàng hơn. Tuy nhiên, đây là điều tôi để lại cho bạn làm.
Ứng dụng giao diện điều khiển
Đây là ứng dụng chính.
Ở đây chúng ta sẽ sử dụng
Trong dự án Ứng dụng bảng điều khiển chính, chúng tôi sẽ tạo tệp NinjectDependencyResolver.cs
. Tập tin này sẽ như sau.
using Ninject.Modules; using ConsoleManager; using ProgramManager; namespace MyConsoleApp { public class NinjectDependencyResolver : NinjectModule { public override void Load() { Bind<IConsoleManager>().To<ConsoleManager.ConsoleManager>(); Bind<IProgramManager>().To<ProgramManager.ProgramManager>(); } } }
Những gì chúng ta có thể nhận thấy ở đây:
- Lớp
NinjectDependencyResolver
đang kế thừaNinjectModule
. - Chúng tôi đang ghi đè phương thức
void Load()
nơi chúng tôi đang đặt các ràng buộc của mình như mong đợi.
Bây giờ, trên Program.cs
:
using Ninject; using System.Reflection; using ProgramManager; namespace MyConsoleApp { class Program { private static IProgramManager m_ProgramManager = null; static void Main(string[] args) { var kernel = new StandardKernel(); kernel.Load(Assembly.GetExecutingAssembly()); m_ProgramManager = kernel.Get<IProgramManager>(); m_ProgramManager.Run(); } } }
Những gì chúng ta có thể nhận thấy ở đây:
- Chúng tôi phụ thuộc vào
IProgramManager
. - Chúng tôi đã tạo bộ chứa IoC thông qua
var kernel = new StandardKernel();
. - Sau đó, chúng tôi đã tải các phụ thuộc vào bộ chứa IoC thông qua
kernel.Load(Assembly.GetExecutingAssembly());
. Điều này hướng dẫn Ninject lấy các liên kết của nó từ tất cả các lớp kế thừaNinjectModule
bên trong hợp ngữ/dự án hiện tại. - Điều này có nghĩa là các liên kết sẽ đến từ lớp
NinjectDependencyResolver
của chúng ta vì nó đang kế thừaNinjectModule
và nằm bên trong hội đồng/dự án hiện tại. - Để lấy một phiên bản của
IProgramManager
chúng tôi đang sử dụng bộ chứa IoC như saukernel.Get<IProgramManager>();
.
Bây giờ, hãy xem liệu thiết kế và công việc chúng tôi đã thực hiện cho đến thời điểm này có khắc phục được sự cố của chúng tôi hay không.
Khoảnh khắc của sự thật
Vì vậy, câu hỏi bây giờ là, chúng ta có thể bao quát Ứng dụng Bảng điều khiển của mình bằng các bài kiểm tra đơn vị không? Để trả lời câu hỏi này, chúng ta hãy thử viết một số bài kiểm tra đơn vị…
Sơ khai hoặc Mocks
Nếu bạn có một số kinh nghiệm với thử nghiệm đơn vị, bạn nên biết rằng chúng tôi có Stub và Mocks được sử dụng để thay thế các phần phụ thuộc của chúng tôi.
Để giải trí, tôi sẽ sử dụng sơ khai cho ví dụ của chúng ta ở đây.
Vì vậy, tôi sẽ định nghĩa ConsoleManagerStub
là sơ khai cho IConsoleManager
như sau:
using System; using System.Collections.Generic; using System.Text; namespace ConsoleManager { public class ConsoleManagerStub : ConsoleManagerBase { private int m_CurrentOutputEntryNumber; private readonly List<string> m_Outputs = new List<string>(); public event Action<int> OutputsUpdated; public event Action OutputsCleared; public Queue<object> UserInputs { get; } = new Queue<object>(); public override void Clear() { m_CurrentOutputEntryNumber++; m_Outputs.Clear(); OnOutputsCleared(); OnOutputsUpdated(m_CurrentOutputEntryNumber); } public override ConsoleKeyInfo ReadKey() { ConsoleKeyInfo result; object input; if (UserInputs.Count > 0) { input = UserInputs.Dequeue(); } else { throw new ArgumentException("No input was presented when an input was expected"); } if (input is ConsoleKeyInfo key) { result = key; } else { throw new ArgumentException("Invalid input was presented when ConsoleKeyInfo was expected"); } return result; } public override string ReadLine() { object input; if (UserInputs.Count > 0) { input = UserInputs.Dequeue(); } else { throw new ArgumentException("No input was presented when an input was expected"); } string result; if (input is string str) { result = str; WriteLine(result); } else { throw new ArgumentException("Invalid input was presented when String was expected"); } return result; } public override void Write(string value) { m_Outputs.Add(value); m_CurrentOutputEntryNumber++; OnOutputsUpdated(m_CurrentOutputEntryNumber); } public override void WriteLine(string value) { m_Outputs.Add(value + "\r\n"); m_CurrentOutputEntryNumber++; OnOutputsUpdated(m_CurrentOutputEntryNumber); } protected void OnOutputsUpdated(int outputEntryNumber) { OutputsUpdated?.Invoke(outputEntryNumber); } protected void OnOutputsCleared() { OutputsCleared?.Invoke(); } public override string ToString() { var result = string.Empty; if (m_Outputs == null || m_Outputs.Count <= 0) return result; var builder = new StringBuilder(); foreach (var output in m_Outputs) { builder.Append(output); } result = builder.ToString(); return result; } } }
Và cuối cùng, các bài kiểm tra đơn vị sẽ như sau:
using ConsoleManager; using NUnit.Framework; using System; using System.Collections.Generic; namespace MyConsoleApp.Tests { [TestFixture] public class ProgramManagerTests { private ConsoleManagerStub m_ConsoleManager = null; private ProgramManager.ProgramManager m_ProgramManager = null; [SetUp] public void SetUp() { m_ConsoleManager = new ConsoleManagerStub(); m_ProgramManager = new ProgramManager.ProgramManager(m_ConsoleManager); } [TearDown] public void TearDown() { m_ProgramManager = null; m_ConsoleManager = null; } [TestCase("Ahmed")] [TestCase("")] [TestCase(" ")] public void RunWithInputAs1AndName(string name) { m_ConsoleManager.UserInputs.Enqueue("1"); m_ConsoleManager.UserInputs.Enqueue(name); m_ConsoleManager.UserInputs.Enqueue(new ConsoleKeyInfo()); var expectedOutput = new List<string> { "Welcome to my console app\r\n", "Welcome to my console app\r\n[1] Say Hello?\r\n", "Welcome to my console app\r\n[1] Say Hello?\r\n[2] Say Goodbye?\r\n", "Welcome to my console app\r\n[1] Say Hello?\r\n[2] Say Goodbye?\r\n\r\n", "Welcome to my console app\r\n[1] Say Hello?\r\n[2] Say Goodbye?\r\n\r\nPlease enter a valid choice: ", "Welcome to my console app\r\n[1] Say Hello?\r\n[2] Say Goodbye?\r\n\r\nPlease enter a valid choice: 1\r\n", "Welcome to my console app\r\n[1] Say Hello?\r\n[2] Say Goodbye?\r\n\r\nPlease enter a valid choice: 1\r\nPlease enter your name: ", "Welcome to my console app\r\n[1] Say Hello?\r\n[2] Say Goodbye?\r\n\r\nPlease enter a valid choice: 1\r\nPlease enter your name: " + name + "\r\n", "Welcome to my console app\r\n[1] Say Hello?\r\n[2] Say Goodbye?\r\n\r\nPlease enter a valid choice: 1\r\nPlease enter your name: " + name + "\r\nHello " + name +"\r\n", "Welcome to my console app\r\n[1] Say Hello?\r\n[2] Say Goodbye?\r\n\r\nPlease enter a valid choice: 1\r\nPlease enter your name: " + name + "\r\nHello " + name +"\r\n\r\n", "Welcome to my console app\r\n[1] Say Hello?\r\n[2] Say Goodbye?\r\n\r\nPlease enter a valid choice: 1\r\nPlease enter your name: " + name + "\r\nHello " + name +"\r\n\r\nPress any key to exit... " }; m_ConsoleManager.OutputsUpdated += outputEntryNumber => { Assert.AreEqual( expectedOutput[outputEntryNumber - 1], m_ConsoleManager.ToString()); }; m_ProgramManager.Run(); } [TestCase("Ahmed")] [TestCase("")] [TestCase(" ")] public void RunWithInputAs2AndName(string name) { m_ConsoleManager.UserInputs.Enqueue("2"); m_ConsoleManager.UserInputs.Enqueue(name); m_ConsoleManager.UserInputs.Enqueue(new ConsoleKeyInfo()); var expectedOutput = new List<string> { "Welcome to my console app\r\n", "Welcome to my console app\r\n[1] Say Hello?\r\n", "Welcome to my console app\r\n[1] Say Hello?\r\n[2] Say Goodbye?\r\n", "Welcome to my console app\r\n[1] Say Hello?\r\n[2] Say Goodbye?\r\n\r\n", "Welcome to my console app\r\n[1] Say Hello?\r\n[2] Say Goodbye?\r\n\r\nPlease enter a valid choice: ", "Welcome to my console app\r\n[1] Say Hello?\r\n[2] Say Goodbye?\r\n\r\nPlease enter a valid choice: 2\r\n", "Welcome to my console app\r\n[1] Say Hello?\r\n[2] Say Goodbye?\r\n\r\nPlease enter a valid choice: 2\r\nPlease enter your name: ", "Welcome to my console app\r\n[1] Say Hello?\r\n[2] Say Goodbye?\r\n\r\nPlease enter a valid choice: 2\r\nPlease enter your name: " + name + "\r\n", "Welcome to my console app\r\n[1] Say Hello?\r\n[2] Say Goodbye?\r\n\r\nPlease enter a valid choice: 2\r\nPlease enter your name: " + name + "\r\nGoodbye " + name + "\r\n", "Welcome to my console app\r\n[1] Say Hello?\r\n[2] Say Goodbye?\r\n\r\nPlease enter a valid choice: 2\r\nPlease enter your name: " + name + "\r\nGoodbye " + name + "\r\n\r\n", "Welcome to my console app\r\n[1] Say Hello?\r\n[2] Say Goodbye?\r\n\r\nPlease enter a valid choice: 2\r\nPlease enter your name: " + name + "\r\nGoodbye " + name + "\r\n\r\nPress any key to exit... " }; m_ConsoleManager.OutputsUpdated += outputEntryNumber => { Assert.AreEqual( expectedOutput[outputEntryNumber - 1], m_ConsoleManager.ToString()); }; m_ProgramManager.Run(); } [Test] public void RunShouldKeepTheMainMenuWhenInputIsNeither1Nor2() { m_ConsoleManager.UserInputs.Enqueue("any invalid input 1"); m_ConsoleManager.UserInputs.Enqueue("any invalid input 2"); m_ConsoleManager.UserInputs.Enqueue("Exit"); var expectedOutput = new List<string> { // initial menu "Welcome to my console app\r\n", // outputEntryNumber 1 "Welcome to my console app\r\n[1] Say Hello?\r\n", // outputEntryNumber 2 "Welcome to my console app\r\n[1] Say Hello?\r\n[2] Say Goodbye?\r\n", // outputEntryNumber 3 "Welcome to my console app\r\n[1] Say Hello?\r\n[2] Say Goodbye?\r\n\r\n", // outputEntryNumber 4 "Welcome to my console app\r\n[1] Say Hello?\r\n[2] Say Goodbye?\r\n\r\nPlease enter a valid choice: ", // outputEntryNumber 5 "Welcome to my console app\r\n[1] Say Hello?\r\n[2] Say Goodbye?\r\n\r\nPlease enter a valid choice: any invalid input 1\r\n", // outputEntryNumber 6 // after first trial "", // outputEntryNumber 7 "Welcome to my console app\r\n", // outputEntryNumber 8 "Welcome to my console app\r\n[1] Say Hello?\r\n", // outputEntryNumber 9 "Welcome to my console app\r\n[1] Say Hello?\r\n[2] Say Goodbye?\r\n", // outputEntryNumber 10 "Welcome to my console app\r\n[1] Say Hello?\r\n[2] Say Goodbye?\r\n\r\n", // outputEntryNumber 11 "Welcome to my console app\r\n[1] Say Hello?\r\n[2] Say Goodbye?\r\n\r\nPlease enter a valid choice: ", // outputEntryNumber 12 "Welcome to my console app\r\n[1] Say Hello?\r\n[2] Say Goodbye?\r\n\r\nPlease enter a valid choice: any invalid input 2\r\n", // outputEntryNumber 13 // after second trial "", // outputEntryNumber 14 "Welcome to my console app\r\n", // outputEntryNumber 15 "Welcome to my console app\r\n[1] Say Hello?\r\n", // outputEntryNumber 16 "Welcome to my console app\r\n[1] Say Hello?\r\n[2] Say Goodbye?\r\n", // outputEntryNumber 17 "Welcome to my console app\r\n[1] Say Hello?\r\n[2] Say Goodbye?\r\n\r\n", // outputEntryNumber 18 "Welcome to my console app\r\n[1] Say Hello?\r\n[2] Say Goodbye?\r\n\r\nPlease enter a valid choice: ", // outputEntryNumber 19 "Welcome to my console app\r\n[1] Say Hello?\r\n[2] Say Goodbye?\r\n\r\nPlease enter a valid choice: Exit\r\n" // outputEntryNumber 20 }; m_ConsoleManager.OutputsUpdated += outputEntryNumber => { if (outputEntryNumber - 1 < expectedOutput.Count) { Assert.AreEqual( expectedOutput[outputEntryNumber - 1], m_ConsoleManager.ToString()); } }; m_ProgramManager.Run(); } } }
Cuối cùng
Giờ đây, chúng tôi đã có thể bao quát Ứng dụng Bảng điều khiển của mình bằng các bài kiểm tra đơn vị. Tuy nhiên, bạn có thể nghĩ rằng điều này là quá nhiều đối với một ứng dụng đơn giản như ứng dụng chúng tôi có ở đây. Đây không phải là quá mức cần thiết?
Trên thực tế, nó phụ thuộc vào những gì bạn muốn trang trải. Ví dụ: trong ứng dụng đơn giản của chúng tôi, tôi đã xử lý mọi ký tự trên giao diện người dùng như một yêu cầu phải được bao phủ bởi các bài kiểm tra đơn vị. Vì vậy, nếu bạn thay đổi một ký tự trong quá trình triển khai chính, thì một bài kiểm tra đơn vị sẽ thất bại.
Có lẽ trong trường hợp của bạn, nó sẽ khác. Tuy nhiên, sẽ luôn tốt nếu bạn biết cách thực hiện ngay cả với ký tự nhỏ 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