+7

03. (UnitTest) - Viết testcase cơ bản với NUnit trong - (Phần 2)

Trong bài trước chúng ta đã tìm hiểu cơ bản về một số thuộc tính trong NUnit và viết được một Testcase đầu tiên.

Bài hôm nay sẽ tiếp tục tìm hiểu về một số thuộc tính và khái niệm nữa trong NUnit.

Viết những TestCase còn lại của method IsValidLogFileName

Chúng ta đã viết một testcase kiểm tra một file không hợp lệ và trả về giá trị là false, như vậy một case đó đã đủ để bao hàm tất cả những trường hợp của method được test chưa? Câu trả lời chắc chắn là chưa.

Vậy làm sao để kiểm tra chúng ta đã viết đủ tất cả testcase để coverage tất cả trường hợp, ta sẽ làm những bước như sau:

  1. Chọn thanh menu Test trên Visual Studio.
  2. Chọn lựa chọn Anylyze Code Coverage For All Tests. image.png
  3. Sau khi phân tích code xong, VS sẽ hiển thị một bảng kết quả code phân tích, trong bảng này sẽ biết được tỉ lệ code bạn viết UnitTest là bao nhiêu % hoặc số dòng được viết so với dự án của bạn. image.png
  4. Chọn đến method được test, nó sẽ hiển thị những phần code nào bạn chưa được Coverage image.png Ta có thể thấy rằng, phần màu xanh là những phần code đã được Coverage, và phần màu vàng chưa được Coverage. Hiển nhiên là với phương thức IsValidLogFileName, chúng ta mới chỉ viết trường hợp là truyền vào một file sai thôi, bây giờ chúng ta sẽ viết TestCase trong trường hợp nhập vào một file hợp lệ.

Chúng ta sẽ viết 2 TestCase tiếp theo, nghiệp vụ của phương thức được Test là kiểm tra một file hợp lệ có đuôi txt hay không. Hiện tại đã viết một phương thức test với đầu vào là file không hợp lệ,bây giờ sẽ viết phương thức test với file có đuôi hợp lệ sẽ là đuôi txt và TXT.

        [Test]
        public void IsValidLogFileName_GoodExtensionLowercase_ReturnsTrue()
        {
            // Arrange
            LogAnalyzer analyzer = new();

            // Act
            bool result = analyzer.IsValidLogFileName("filewithgoodextension.txt");

            // Assert
            Assert.That(result, Is.True);
        }

        [Test]
        public void IsValidLogFileName_GoodExtensionUppercase_ReturnsTrue()
        {
            // Arrange
            LogAnalyzer analyzer = new();

            // Act
            bool result = analyzer.IsValidLogFileName("filewithgoodextension.TXT");

            // Assert
            Assert.That(result, Is.True);
        }

Nội dung 2 phương thức trên đều có comment thành 3 phần rõ ràng theo AAA pattern, và với những bạn mới viết UnitTest thì nên chia rõ ràng như vậy để rõ ràng và dễ đọc.

Sau khi run lại tests, kết quả có một TestCase lỗi như sau: image.png

Khẳng định của TestCase đã sai, ko như mong đợi chúng ta đang cần. Nguyên nhân là do phương thức được test chúng ta đã code thiếu nghiệp vụ kiểm tra những file của đuôi TXT là trường hợp in hoa. Từ vấn đề này cũng khẳng định một ưu điểm của UnitTest là có thể tìm thấy lỗi Bussiness mà chúng ta không mong muốn để sửa lại và tăng chất lượng của phương thức. Hàm được test sẽ được viết lại như sau:

        public bool IsValidLogFileName(string fileName)
        {
            if (!fileName.EndsWith(".txt", StringComparison.CurrentCultureIgnoreCase))
            {
                return false;
            }
            return true;
        }

Kết quả là 3 TestCase đã pass và độ bao phủ của phương thức đã là 100%: image.png

image.png

Sử dụng Parameterized Tests với TestCase attribute

Như vậy chúng ta đã viết được 3 TestCases, với một nghiệp vụ đơn giản là kiểm tra file đuôi .txt, điều này đã gặp một vấn đề cho việc bảo trì và phát triển sau này. Với 3 trường hợp thì và những giá trị khác nhau như vậy thì ko tệ lắm, nhưng nếu 10, 30, 100.. trường hợp thì sẽ ra sao? Ta sẽ phải viết 100 TestCase y hệt, chỉ khác giá trị của file truyền vào.

NUnit có một tính năng để hỗ trợ việc này, nó gọi là: parameterized tests. Ta chỉ cần duy nhất một phương thức test sau khi refactor, các bước như sau:

  1. Thay thế [Test] attribue thành [TestCase] attribute.
  2. Lấy tất cả những giá trị hardcode mà test đang sử dụng thành param của phương thức Test.
  3. Di chuyển các giá trị param vào trong dấu ngoặc () của TestCase - [TestCase(param1, param2,..)]
  4. Đổi lại tên phương thức test sao cho mang tính chất generic hơn.
  5. Thêm từng [TestCase] tương đương với từng thử nghiệm muốn kiểm tra trên phương thức Test.
  6. Xóa các thử nghiệm khác để bạn chỉ còn lại một phương pháp thử nghiệm có nhiều thuộc tính [TestCase].

Tham số được gửi vào thuộc tính TestCase sẽ được trình chạy thử nghiệm ánh xạ đến tham số của phương thức thử nghiệm theo thứ tự tương ứng. Bạn có thể thêm bao nhiêu tham số tùy thích và phương thức thử nghiệm cũng như TestCase attribute.

Phương thức Test được viết lại và vẫn tạo ra 2 TestCase như ta mong muốn nhưng code đã dễ đọc và dễ bảo trì hơn, như sau:

        [TestCase("filewithgoodextension.txt")]
        [TestCase("filewithgoodextension.TXT")]
        public void IsValidLogFileName_ValidExtensions_ReturnsTrue(string file)
        {
            // Arrange
            LogAnalyzer analyzer = new();

            // Act
            bool result = analyzer.IsValidLogFileName(file);

            // Assert
            Assert.That(result, Is.True);
        }

Setup and teardown attribute

Đối với thử nghiệm đơn vị, điều quan trọng là mọi dữ liệu hoặc instance từ những method test trước đó đều phải được loại bỏ, thử nghiệm mới được tạo với trạng thái mới nhất giống như chưa từng có thử nghiệm nào được chạy trước đó.

Trong NUnit, có các attribute đặc biệt cho phép kiểm soát dễ dàng hơn việc thiết lập và xóa trạng thái trước và sau khi kiểm tra. Đây là các attribute hành động [SetUp] và [TearDown]. Hình sau cho thấy quá trình chạy thử nghiệm với các hành động thiết lập và phá bỏ.

image.png

Hiện tại, hãy đảm bảo rằng mỗi bài kiểm tra bạn viết sử dụng một instance mới của lớp đang được kiểm tra, để không còn trạng thái hay dữ liệu nào còn sót lại có thể làm lỗi bài kiểm tra của bạn.

[SetUp]: attribute này có thể được đặt trên một phương thức Setup, giống như attribute [Test] và nó khiến NUnit chạy phương thức thiết lập đó mỗi khi nó chạy bất kỳ thử nghiệm nào trong lớp của bạn.

[TearDown]: attribute này biểu thị một phương thức sẽ được thực thi một lần sau mỗi testcase trong lớp của bạn được đã được thực thi xong.

    [TestFixture]
    public class LogAnalyzerTests
    {
        private LogAnalyzer m_analyzer;

        [SetUp]
        public void Setup()
        {
            m_analyzer = new LogAnalyzer();
        }


        [Test]
        public void IsValidFileName_BadExtension_ReturnsFalse()
        {
            // Act
            bool result = m_analyzer.IsValidLogFileName("filewithbadextension.foo");

            // Assert
            Assert.That(result, Is.False);
        }

        [TestCase("filewithgoodextension.txt")]
        [TestCase("filewithgoodextension.TXT")]
        public void IsValidLogFileName_ValidExtensions_ReturnsTrue(string file)
        {
            // Act
            bool result = m_analyzer.IsValidLogFileName(file);

            // Assert
            Assert.That(result, Is.True);
        }
        
        [TearDown]
        public void TearDown()
        {
            m_analyzer = null;
        }        
    }

Hãy coi phương thức SetUp như là phương thức contructor của mỗi lớp, lớp class test sẽ chỉ có một phương thức setup, còn TearDown là Detructor của class test.

Luồng hoạt động của đoạn code test trên sẽ như sau: image.png

Thuộc tính TearDown hầu như không được sử dụng ở trong các dự án thực tế, việc giới thiệu thuộc tính trong bài viết này chỉ giúp bạn hiểu thêm về các thuộc tính trong NUnit và cách hoạt động của nó. Thông thường các sử dụng duy nhất của nó là đặt lại trạng thái của biến static hoặc instanse singleton của một service trong bộ nhớ giữa các testcase với nhau.

Tiếp theo, chúng ta sẽ xem cách bạn có thể kiểm tra xem code của bạn có đưa ra ngoại lệ hay không khi cần thiết.

Kiểm tra kết quả là ngoại lệ

Một trong những kịch bản phổ biến là kiểm tra xem phương thức test có đưa ra một ngoại lệ như mong muốn với case của phương thức được test hay không?

Giả sử phương thức kiểm tra file mà chúng ta đã đi qua nãy giờ văng ra ngoại lệ ArgumentException nếu bạn truyền vào một tên file rỗng hoặc null.

        public bool IsValidLogFileName(string fileName)
        {
            // Kiểm tra ngoại lệ ở đây
            if (string.IsNullOrEmpty(fileName))
            {
                throw new ArgumentException(
                "filename has to be provided");
            }

            if (!fileName.EndsWith(".txt", StringComparison.CurrentCultureIgnoreCase))
            {
                return false;
            }
            return true;
        }

Thì khi đó phương thức test phải đảm bảo là, với tên file rỗng hoặc null, một khẳng định là kết quả của một ngoại lệ phải được kiểm tra. Nếu đoạn code không văng ra ngoại lệ thì test của bạn đã fail.

Để kiểm tra ngoại lệ chúng ta sẽ sử dụng API: Assert.Catch<T>(delegate). của NUNit.

        [Test]
        public void IsValidFileName_EmptyFileName_Throws()
        {
            var ex = Assert.Catch<Exception>(() => m_analyzer.IsValidLogFileName(""));
            StringAssert.Contains("filename has to be provided", ex.Message);
        }

Nếu Assert.Catch không trả về một ngoại lệ thì test case bạn viết để kiểm tra ngoại lệ trong trường hợp file rỗng sẽ fail, ngược lại nó trả về một instance Exception, từ instance nè bạn có thể kiểm tra xem Message văng ra có đúng như mong muốn hay không?

Bỏ qua test case

Đôi bạn có một vài tests có vấn đề, và bạn vẫn muốn check in code của mình lên source control (trường hợp nè không được khuyến khích), bạn có thể đặt thuộc tính [Ignore] cho test case trong trường hợp test case lỗi chứ không phải do code test lỗi. Nó trông sẽ như sau:


        [Test]
        [Ignore("Tạm thời bò qua test case nè")]
        public void IsValidFileName_ValidFile_ReturnsTrue()
        {
            /// ...
        }

Sau khi chạy, GUI của NUnit sẽ hiển thị như sau với những test case được bỏ qua: image.png

NUnit’s với fluent API

NUnit cũng cấp các fluent API giúp lời gọi đơn giản hơn, chỉ bằng cách gọi Assert.* method, nó luôn được bắt đầu với Assert.That(..). Đây là test case được viết lại với cú pháp fluent của NUnit:

[Test]
public void IsValidFileName_EmptyFileName_ThrowsFluent()
{
var ex = Assert.Catch<ArgumentException>(() => m_analyzer.IsValidLogFileName(""));
Assert.That(ex.Message, Does.Contain("filename has to be provided"), "filename has to be provided");
}

Việc giới thiệu Assert.That ở đây giúp bạn có nhiều cách xử lý và tổng quan để tìm hiểu thêm chi tiết. Cá nhân tôi thích việc sử dụng Assert.something() hơn Assert.That. Mặc dù, trông có vẻ fluent API có vẻ thân thiện hơn nhưng sẽ mất nhiều thời gian hơn để hiểu nó làm gì cho đến khi đọc đến cuối, chọn theo ý muốn nhưng bạn cần đảm bảo chọn 1 phương pháp sẽ nhất quán trên toàn ứng dụng.

Cài đặt theo từng chuyên mục

Bạn có thể thiết lập các test case của mình để chạy theo các danh mục cụ thể, ví dụ:

        [Test]
        [Category("Chuyên mục 1")]
        public void IsValidFileName_ValidFile_ReturnsTrue()
        {
            /// ...
        }

Đoạn code trên chia test case thành chuyên mục có tên là: Chuyên mục 1, sau khi phân loại chuyên mục bạn có thể lọc dễ dàng trên GUI của NUnit để lọc ra những chuyên mục bạn quan tâm. image.png

Testing trong trường hợp thay đổi trạng thái của hệ thống

Trong những bài trước, chúng ta đã biết được việc gọi một phương thức test thì nó sẽ có 3 trường hợp kết quả xảy ra. Chúng ta đã tìm hiểu được trường hợp đầu tiên là gọi một phương thức và trả về một kết quả có thể kiểm tra được (Value-base). Bây giờ chúng ta sẽ tìm hiểu trường hợp thứ hai là kiểm thử trường hợp thay đổi giá trị, hành vi của một hệ thống (State-base).

State-based testing: là kiểm tra phương thức có hoạt động chính xác hay không bằng cách kiểm tra hành vi đã thay đổi của một hệ thống được kiểm tra và các phụ thuộc liên quan đến nó sau khi phương thức được thực thi. Nếu hệ thống hoạt động giống hệt như trước đây thì bạn thực sự không thay đổi trạng thái của nó hoặc có lỗi.

Chúng ta sẽ chỉnh sửa một chút lớp LogAnalyzer:

    public class LogAnalyzer
    {
        public bool WasLastFileNameValid { get; set; }

        public bool IsValidLogFileName(string fileName)
        {
            WasLastFileNameValid = false; // Changes the state of the system

            if (string.IsNullOrEmpty(fileName))
            {
                throw new ArgumentException("filename has to be provided");
            }
            if (!fileName.EndsWith(".SLF",StringComparison.CurrentCultureIgnoreCase))
            {
                return false;
            }

            WasLastFileNameValid = true; // Changes the state of the system
            return true;
        }
    }

Ở đây ta thêm một biến WasLastFileNameValid để ghi nhớ kết quả cuối cùng của file hợp lệ hay không, và ở trong phương thức IsValidLogFileName chúng ta có thay đổi biển đó. Đây là một trường hợp mà hàm thay đổi giá trị các thành viên trong hệ thống.

Test case đơn giản để kiểm tra WasLastFileNameValid có được gán lại kết quả đúng hay không, được viết như sau:


        [Test]
        public void IsValidFileName_WhenCalled_ChangesWasLastFileNameValid()
        {
            m_analyzer.IsValidLogFileName("badname.foo");
            Assert.False(m_analyzer.WasLastFileNameValid);
        }

Việc đặt tên kịch bản trong những trường hợp thay đổi trạng thái của hệ thống thường đặt như sau:

  • ByDefault: được sử dụng khi kết quả mong đợi là trường hợp mà chưa thay đổi hành vi của hệ thống, ví dụ trong trường hợp trên, khi chưa thay đổi biến WasLastFileNameValid, ta có một test case nào đó cần kiểm tra là WasLastFileNameValid = false thì tên phương thức test sẽ là IsValidFileName_ByDefault_ChangesWasLastFileNameValid

  • WhenCalled hoặc Always: thường sử dụng khi kiểm thử case gọi phương thức thay đổi hành vi, trạng thái của hệ thống hoặc gọi đến một bên thứ 3.

    Nhưng điều gì sẽ xảy ra khi phương thức bạn đang thử nghiệm phụ thuộc vào tài nguyên bên ngoài, chẳng hạn như hệ thống tệp, cơ sở dữ liệu, dịch vụ web hoặc bất kỳ thứ gì khác mà bạn khó kiểm soát? Và làm cách nào để bạn kiểm tra loại kết quả, cuộc gọi tới bên thứ ba? Đó là khi bạn bắt đầu với việc tạo stubs, fake objects, và mock objects. Chúng ta sẽ bàn luận ở bài sau.

Tổng kết

Trong bài này chúng ta tìm hiểu thêm về một số thuộc tính của NUnit và các sử dụng của chúng. Trong những tình huống của dự án thực tế thì hầu như chỉ sử dụng những thuộc tính được giới thiệu trong bài này. Trong bài tiếp theo chúng ta sẽ tìm hiểu về các làm việc với đối tượng giả.

Nếu thấy hay mọi người có thể đăng ký kênh ytb của mình.

Tại đây!

,


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí