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.
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…
Đầ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.
Đây là kế hoạch của chúng tôi:
Đơ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:
System.Console
tĩnh.System.Console
. 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?
System.Console
.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.
Bây giờ, hãy chia giải pháp của chúng ta thành các mô-đun nhỏ hơ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:
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 khai IConsoleManager
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ọc System.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:
IConsoleManager
.IConsoleManager
trong khi viết bài kiểm tra đơn vị.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.Đâ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:
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 khai IProgramManager
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ào IConsoleManager
.
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:
ProgramManager
vào IConsoleManager
.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ị.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.
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.
Đâ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:
NinjectDependencyResolver
đang kế thừa NinjectModule
.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:
IProgramManager
.var kernel = new StandardKernel();
.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ừa NinjectModule
bên trong hợp ngữ/dự án hiện tại.NinjectDependencyResolver
của chúng ta vì nó đang kế thừa NinjectModule
và nằm bên trong hội đồng/dự án hiện tại.IProgramManager
chúng tôi đang sử dụng bộ chứa IoC như sau kernel.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.
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ị…
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(); } } }
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