Sử dụng xUnit để kiểm tra mã C# của bạn

Sử dụng xUnit để kiểm tra mã C# của bạn

Bài viết này sẽ hướng dẫn bạn tạo các bài kiểm tra tự động với xUnit cho các ứng dụng C# của bạn. Bạn sẽ học những kiến ​​thức cơ bản về tự động hóa kiểm thử và cách tạo kiểm thử đơn vị (unit test) và kiểm thử tích hợp (integration test).

Kiến thức cơ bản về tự động hóa kiểm thử

Kiểm thử đảm bảo rằng mã ứng dụng của bạn đang làm đúng những gì nó phải làm. Mã ứng dụng có thể là những câu lệnh tầm thường, nhưng đôi khi những câu lệnh này bị đánh giá thấp, đặc biệt là khi bạn thay đổi codebase hiện có của mình.

Bạn phải đảm bảo rằng các thay đổi của bạn không chỉ hoạt động như dự định mà còn phải đảm bảo rằng những đoạn mã khác tiếp tục thực hiện công việc mong đợi của nó.

Kiểm tra thủ công là một công việc rất khó khăn, không chỉ để thực hiện các bài kiểm tra mà còn vì bạn phải thực hiện chúng rất nhiều lần. Đó là một công việc lặp đi lặp lại và khi có một công việc lặp đi lặp lại, bạn cần tự động hóa.

Theo truyền thống, có một số loại kiểm thử tự động khác nhau. Hãy cùng xem nhanh các định nghĩa của những cái phổ biến nhất:

  • Unit test. Loại kiểm thử này tập trung vào việc kiểm tra một đơn vị mã: một khối xây dựng của ứng dụng phần mềm, chẳng hạn như một hàm hoặc một lớp. Unit test đảm bảo rằng một thành phần biệt lập của ứng dụng phần mềm hoạt động như mong đợi.
  • Integration test. Không giống như Unit test, kiểm thử tích hợp (Integration test) giúp phát hiện bất kỳ vấn đề nào khi các đơn vị mã được lắp ráp lại với nhau để tạo ra các thành phần phức tạp hơn. Trên thực tế, ngay cả khi mỗi đơn vị mã hoạt động chính xác trong một môi trường biệt lập, bạn có thể phát hiện ra một số vấn đề khi kết hợp chúng lại với nhau để xây dựng ứng dụng của mình.
  • End-to-end (E2E) test. Đây là một loại kiểm thử đảm bảo rằng một chức năng cấp người dùng hoạt động như mong đợi. Ở một mức độ nào đó, chúng tương tự như các bài kiểm tra tích hợp. Tuy nhiên, ở đây tập trung vào các chức năng mà người dùng phần mềm có thể truy cập trực tiếp hoặc bằng cách nào đó từ bên ngoài ứng dụng. Một bài kiểm tra E2E có thể liên quan đến nhiều hệ thống và nhằm mục đích mô phỏng một kịch bản sản xuất (production).

Nhiều định nghĩa kiểm thử khác tồn tại dựa trên các mục tiêu thử nghiệm và quan điểm mà bạn nhìn vào chúng. Nhưng những cái trên đại diện cho những cái phổ biến nhất theo quan điểm của nhà phát triển.

Tất nhiên, mỗi loại kiểm thử mang lại giá trị để đảm bảo tính đúng đắn của ứng dụng phần mềm, và mỗi loại đều có điểm mạnh và điểm yếu. Ví dụ, trong khi các bài kiểm tra đơn vị thường được thực hiện rất nhanh, các bài kiểm tra E2E lại chậm hơn và có thể có nhiều điểm lỗi khác nhau do sự tương tác của nhiều hệ thống.

Khi kiểm tra hệ thống của mình, bạn không thể giả vờ có thể bao quát tất cả các trường hợp sử dụng có thể xảy ra. Bạn nên giới hạn chúng trong một tập hợp con do sự gia tăng độ phức tạp khi chuyển từ một đơn vị đơn giản sang một thành phần của hệ thống, một phần là do thời gian cần thiết để thực hiện các bài kiểm tra.

Thông thường, số lượng bài kiểm tra giảm trong khi chuyển từ bài kiểm tra đơn vị đến bài kiểm tra E2E, theo sơ đồ Kim tự tháp kiểm tra nổi tiếng:

Sử dụng xUnit để kiểm tra mã C# của bạn

Về cách cấu trúc các bài kiểm tra tự động của bạn, một cách tiếp cận điển hình tuân theo cái gọi là mẫu AAA. Tên bắt nguồn từ chữ cái đầu của ba hành động thường cần để thực hiện kiểm thử:

  • Arrange. Với hành động này, bạn chuẩn bị tất cả các dữ liệu cần thiết và các điều kiện tiên quyết.
  • Act. Hành động này thực hiện kiểm tra thực tế.
  • Assert. Hành động cuối cùng này kiểm tra xem kết quả mong đợi đã xảy ra hay chưa.

Trong suốt bài viết này, bạn sẽ sử dụng mẫu này để viết các bài kiểm tra của mình.

Tạo project unit test đầu tiên của bạn với xUnit

Nền tảng .NET Core hỗ trợ các framework kiểm thử khác nhau. Tuy nhiên, xUnit đã trở nên phổ biến nhất do tính đơn giản, mạnh mẽ và khả năng mở rộng của nó.

Dự án được hỗ trợ bởi .NET Foundation và là một phần của các phiên bản .NET Core mới hơn. Điều này có nghĩa là bạn không cần cài đặt bất cứ thứ gì ngoài .NET Core SDK.

Thiết lập dự án để kiểm thử

Để hiểu cách sử dụng xUnit để tự động hóa các bài kiểm tra của bạn, hãy cùng khám phá những điều cơ bản bằng cách tạo các bài kiểm tra đơn vị cho một dự án hiện có.

Bạn có thể đã nghe nói về Phát triển theo hướng kiểm thử (TDD – Test-Driven Development). Nó là một quá trình phát triển phần mềm nhằm thúc đẩy việc viết các bài kiểm tra trước khi viết mã ứng dụng của bạn. Cách tiếp cận này dẫn đến một chu kỳ phát triển ngắn và lặp đi lặp lại dựa trên việc viết một bài kiểm tra và để nó không thành công, sửa lỗi bằng cách viết mã ứng dụng và cấu trúc lại mã ứng dụng để dễ đọc và hiệu suất cao.

Bài viết này sẽ hướng dẫn bạn viết các bài kiểm tra mà không đề cập bất kỳ cách tiếp cận cụ thể nào để phát triển phần mềm.

Để minh họa tạo unit test sử dụng xUnit, chúng ta sẽ tạo một project .NET Core Console tên là Validator.Password. Project này là một thư viện rất đơn giản để xác thực mật khẩu với những ràng buộc sau:

  • Độ dài mật khẩu phải có ít nhất 8 ký tự và tối đa là 20 ký tự.
  • Mật khẩu phải chứa một hoặc nhiều ký tự viết hoa.
  • Mật khẩu phải chứa một hoặc nhiều ký tự viết thường.
  • Mật khẩu phải chứa một hoặc nhiều chữ số.
  • Mật khẩu phải chứa một hoặc nhiều ký tự đặc biệt trong danh sách @ #! $%

Việc triển khai nó dựa trên lớp sau được định nghĩa trong tập tin PasswordValidator.cs:

using System.Text.RegularExpressions;

namespace Validators.Password
{
    public class PasswordValidator
    {
        public bool IsValid(string password)
        {
            Regex passwordPolicyExpression = new Regex(@"((?=.*d)(?=.*[a-z])(?=.*[A-Z])(?=.*[@#!$%]).{8,20})");
            return passwordPolicyExpression.IsMatch(password);
        }
    }
}

Như bạn có thể thấy, logic xác thực được thực hiện bởi phương thức IsValid() thông qua một biểu thức chính quy. Ở đây, lớp PasswordValidator đại diện cho một đơn vị mã vì nó khép kín và tập trung vào một mục tiêu cụ thể.

Tạo project kiểm thử bằng xUnit

Để đảm bảo rằng phương thức IsValid() đang hoạt động như bạn mong đợi, bạn cần tạo một project để kiểm thử.

Đảm bảo bạn đang ở trong thư mục project, gõ các lệnh sau trong cửa sổ dòng lệnh:

dotnet new xunit -o PasswordValidator.Tests
dotnet add ./PasswordValidator.Tests/PasswordValidator.Tests.csproj reference ./PasswordValidator/PasswordValidator.csproj

Lệnh đầu tiên tạo project unit test, trong khi lệnh thứ hai thêm vào đó một tham chiếu đến dự án PasswordValidator.

Hoặc đơn giản nhất là trong Visual Studio bạn thêm project mới vào dự án với mẫu project như sau:

Tạo project kiểm thử bằng xUnit

Sau khi tạo project kiểm thử bằng xUnit xong, bạn add project Validator.Password vào project này.

Kiểm thử trường hợp thành công

Bây giờ chúng ta sẽ đổi tên tập tin PasswordValidator.Tests/UnitTest1.cs thành PasswordValidator.Tests/ValidityTests.cs và thay thế nội dung của tập tin như sau:

using System;
using Xunit;
using Validators.Password;

namespace PasswordValidatorTests
{
    public class ValidityTest
    {
        [Fact]
        public void ValidPassword()
        {
            //Arrange
            var passwordValidator = new PasswordValidator();
            const string password = "Th1sIsapassword!";

            //Act
            bool isValid = passwordValidator.IsValid(password);

            //Assert
            Assert.True(isValid, $"The password {password} is not valid");
        }
    }
}

Ở đây bạn sẽ thấy lớp ValidityTest đang chứa bài kiểm tra unit test cho phương thức IsValid() của lớp PasswordValidator.

Bài kiểm tra unit test duy nhất hiện được triển khai là phương thức ValidPassword(). Phương thức này có attribute Fact, cho xUnit biết rằng đây là một bài kiểm tra.

Các câu lệnh trong phần thân của phương thức ValidPassword() được sắp xếp để làm nổi bật mẫu AAA được đề cập ở trên.

Trong bước Arrange, bạn tạo một thể hiện của lớp PasswordValidator và định nghĩa một mật khẩu hợp lệ.

Trong bước Act, bạn gọi phương thức IsValid() với mật khẩu đã định nghĩa trước đó.

Cuối cùng, bước Assert xác minh rằng kết quả trả về là kết quả mong đợi. Kiểm tra này sử dụng đối tượng Assert, đối tượng này cung cấp nhiều phương thức để xác minh kết quả.

Trong trường hợp này, bạn đang sử dụng phương thức True(), phương thức này thành công khi đối số đầu tiên của nó là true. Nếu không, kiểm tra không thành công và hiển thị thông điệp được cung cấp dưới dạng đối số thứ hai.

Để chạy unit test đầu tiên này, hãy đảm bảo bạn đang ở trong thư mục của project PasswordValidator.Tests và nhập lệnh sau vào cửa sổ đầu cuối của bạn:

dotnet test

Hoặc trong Visual Studio bạn click chuột phải vào project PasswordValidator.Tests rồi chọn Run Tests. Bạn sẽ thấy một cái gì đó tương tự như sau trong bảng điều khiển của bạn:

Microsoft (R) Test Execution Command Line Tool Version 16.3.0
Copyright (c) Microsoft Corporation.  All rights reserved.

Starting test execution, please wait...

A total of 1 test files matched the specified pattern.
                                                                                
Test Run Successful.
Total tests: 1
     Passed: 1
 Total time: 1.1692 Seconds

Xin chúc mừng, bài kiểm tra unit test đầu tiên của bạn đã vượt qua!

Tips: để xem nhanh tất cả các bài kiểm tra unit test và kết quả chạy các bài kiểm tra này trong Visual Studio bạn truy cập menu View -> Test Explorer (hoặc nhấn Ctrl + E, T).

Test Explorer trong Visual Studio

Kiểm thử trường hợp thất bại

Khi bạn đang kiểm tra mã của mình, bạn không nên chỉ xác minh các trường hợp thành công; có nghĩa là những trường hợp mà mọi thứ vẫn ổn.

Bạn cũng cần phải xác minh các trường hợp thất bại. Đối với phương thức IsValid(), bạn phải xác minh một trường hợp có thể xảy ra khi mật khẩu được truyền dưới dạng đối số không tuân thủ các ràng buộc.

Vì vậy, hãy thêm vào bài kiểm tra unit test mới đó là thêm phương thức NotValidPassoword() vào lớp ValidityTest, như được hiển thị bên dưới:

using System;
using Xunit;
using Validators.Password;

namespace PasswordValidatorTests
{
    public class ValidityTest
    {
        [Fact]
        public void ValidPassword()
        {
            // ...code...
        }

        [Fact]
        public void NotValidPassword()
        {
            //Arrange
            var passwordValidator = new PasswordValidator();
            const string password = "thisIsaPassword";

            //Act
            bool isValid = passwordValidator.IsValid(password);

            //Assert
            Assert.False(isValid, $"The password {password} should not be valid!");
        }
    }
}

Trong trường hợp này, bạn truyền một mật khẩu không hợp lệ và trong bước Assert, bạn sẽ sử dụng phương thức False để chỉ định rằng giá trị được phương thức IsValid() trả về là false. Nếu bạn chạy lại các bài kiểm tra unit test, bạn sẽ nhận được hai bài kiểm tra thành công.

Tạo Theory trong xUnit

Hai trường hợp về tính hợp lệ của mật khẩu được kiểm tra bởi các bài kiểm tra unit test ở trên là không đầy đủ.

Chúng chỉ là hai ví dụ đơn giản về các trường hợp thành công và thất bại, nhưng tất nhiên, các trường hợp có thể để kiểm tra còn nhiều hơn thế nữa.

Bạn không thể mong đợi có thể kiểm tra hết mọi trường hợp có thể xảy ra, nhưng bạn có thể kiểm tra một tập hợp con đáng kể các trường hợp điển hình.

Điều này giúp bạn có phạm vi mã được kiểm tra lớn hơn cho mã sản xuất của bạn. Trong ví dụ xác thực mật khẩu, điều này có nghĩa là bạn nên xác định một tập mật khẩu hợp lệ và không hợp lệ đại diện. Đối với mỗi mật khẩu trong các tập này, bạn nên áp dụng một trong các bài kiểm tra được triển khai ở trên.

Cách tiếp cận này sẽ đảm bảo sự tin cậy đáng kể vào hoạt động chính xác của phương thức IsValid(). Nhưng nó yêu cầu phải sao chép cùng một mã cho mỗi mật khẩu mẫu để kiểm tra. Bạn biết rằng sao chép mã không phải là một best practice.

May mắn thay, xUnit có thể giúp bạn vấn đề này bằng cách sử dụng attribute Theory. Theory là một bài kiểm tra unit test có tham số cho phép bạn thực hiện một tập hợp các bài kiểm tra đơn vị có cùng cấu trúc.

Theory cho phép bạn triển khai cái được gọi là kiểm thử theo hướng dữ liệu, là một phương pháp kiểm thử dựa nhiều vào sự biến đổi dữ liệu đầu vào.

Vì vậy, để dễ hình dung Theory là gì, hãy thay thế nội dung của tập tin ValidityTests.cs bằng nội dung sau:

using System;
using Xunit;
using Validators.Password;

namespace PasswordValidatorTests
{
    public class ValidityTest
    {
        [Theory]
        [InlineData("Th1sIsapassword!")]
        [InlineData("thisIsapassword123!")]
        [InlineData("Abc$123456")]
        public void ValidPassword(string password)
        {
            //Arrange
            var passwordValidator = new PasswordValidator();

            //Act
            bool isValid = passwordValidator.IsValid(password);
            
             //Assert
            Assert.True(expectedResult);
        }
        
        [Theory]
        [InlineData("Th1s!")]
        [InlineData("thisIsAPassword")]
        [InlineData("thisisapassword#")]
        [InlineData("THISISAPASSWORD123!")]
        [InlineData("")]
        public void NotValidPassword(string password)
        {
            //Arrange
            var passwordValidator = new PasswordValidator();

            //Act
            bool isValid = passwordValidator.IsValid(password);
            
             //Assert
            Assert.False(expectedResult);
        }
    }
}

Đoạn mã trên thay thế attribute Fact bằng attribute Theory, bổ sung thêm các attribute InlineData và tham số password vào 2 bài kiểm tra unit test.

Như bạn có thể thấy, thay vì hard code thông tin mật khẩu thì ở ví dụ trên, chúng ta tạo tham số password để lấy thông tin mật khẩu từ attribute InlineData. Mỗi attribute InlineData sẽ đại diện cho một mật khẩu trong tập mật khẩu chúng ta cần kiểm tra.

Nói cách khác, mỗi attribute InlineData đại diện cho một lệnh gọi của bài kiểm tra. Trên thực tế, nếu bạn khởi chạy lệnh, bạn sẽ nhận được thông báo rằng tất cả 3 bài kiểm tra ValidPassword và 5 bài kiểm tra NotValidPassword đều thành công.

Bên cạnh attribute InlineData, xUnit cung cấp cho bạn các cách khác để định nghĩa dữ liệu cho Theory, chẳng hạn như nguồn dữ liệu ClassData là một lớp triển khai interface IEnumerable và nguồn dữ liệu MemberData là một thuộc tính hoặc một phương thức.

Trả lời

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *