Làm thế nào tôi có thể đơn vị phương pháp thử nghiệm đang sử dụng phương pháp tĩnh?


8

Giả sử tôi đã viết một phương thức mở rộng trong C # cho bytecác mảng mã hóa chúng thành các chuỗi hex, như sau:

public static class Extensions
{
    public static string ToHex(this byte[] binary)
    {
        const string chars = "0123456789abcdef";
        var resultBuilder = new StringBuilder();
        foreach(var b in binary)
        {
            resultBuilder.Append(chars[(b >> 4) & 0xf]).Append(chars[b & 0xf]);
        }
        return resultBuilder.ToString();
    }
}

Tôi có thể kiểm tra phương pháp trên bằng NUnit như sau:

[Test]
public void TestToHex_Works()
{
    var bytes = new byte[] { 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef };
    Assert.AreEqual("0123456789abcdef", bytes.ToHex());
}

Nếu tôi sử dụng Extensions.ToHexbên trong dự án của mình, hãy giả sử theo Foo.Dophương pháp như sau:

public class Foo
{
    public bool Do(byte[] payload)
    {
        var data = "ES=" + payload.ToHex() + "ff";
        // ...
        return data.Length > 5;
    }
    // ...
}

Sau đó, tất cả các bài kiểm tra Foo.Dosẽ phụ thuộc vào sự thành công của TestToHex_Works.

Sử dụng các hàm miễn phí trong C ++ , kết quả sẽ giống nhau: các thử nghiệm mà các phương thức kiểm tra sử dụng các hàm miễn phí sẽ phụ thuộc vào sự thành công của các thử nghiệm chức năng miễn phí.

Làm thế nào tôi có thể xử lý các tình huống như vậy? Tôi có thể bằng cách nào đó giải quyết các phụ thuộc kiểm tra? Có cách nào tốt hơn để kiểm tra các đoạn mã ở trên không?


4
Then all tests of Foo.Do will depend on the success of TestToHex_works-- Vì thế? Bạn không có các lớp học phụ thuộc vào sự thành công của các lớp khác?
Robert Harvey

5
Tôi chưa bao giờ hiểu nỗi ám ảnh này với các chức năng tự do / tĩnh và cái gọi là không thể kiểm tra của chúng. Nếu một chức năng miễn phí không có tác dụng phụ, thì đó là điều dễ nhất trên hành tinh để kiểm tra và chứng minh rằng nó hoạt động. Bạn đã chứng minh điều này khá hiệu quả trong câu hỏi của riêng bạn. Làm thế nào để bạn kiểm tra các phương thức miễn phí, hiệu ứng phụ thông thường (không phụ thuộc vào trạng thái lớp) trong các trường hợp đối tượng? Tôi biết bạn có một số trong số đó.
Robert Harvey

2
Nhược điểm duy nhất của mã này khi sử dụng các hàm tĩnh là bạn không thể dễ dàng sử dụng cái gì khác ngoài toHex(hoặc thực hiện trao đổi). Ngoài ra mọi thứ đều ổn. Mã của bạn chuyển đổi thành hex đã được kiểm tra, giờ đây có một mã khác sử dụng mã được kiểm tra đó làm tiện ích để đạt được mục tiêu của riêng mình.
Steve Chamaillard

3
Tôi hoàn toàn thiếu những gì là vấn đề ở đây. Nếu ToHex không hoạt động, thì rõ ràng là Do đó cũng không hoạt động.
Simon B

5
Thử nghiệm cho Foo.Do () không nên biết hoặc quan tâm rằng nó gọi ToHex () dưới vỏ bọc, đó là một chi tiết triển khai.
17 của 26

Câu trả lời:


37

Sau đó, tất cả các bài kiểm tra Foo.Dosẽ phụ thuộc vào sự thành công của TestToHex_Works.

Đúng. Đó là lý do tại sao bạn có bài kiểm tra TextToHex. Nếu các thử nghiệm đó vượt qua, chức năng đáp ứng thông số kỹ thuật được xác định trong các thử nghiệm đó. Vì vậy, Foo.Docó thể gọi nó một cách an toàn và không lo lắng về nó. Nó đã được bảo hiểm rồi.

Bạn có thể thêm một giao diện, biến phương thức thành một phương thức cá thể và đưa nó vào Foo. Sau đó, bạn có thể chế giễu TextToHex. Nhưng bây giờ bạn phải viết một bản giả, có thể hoạt động khác nhau. Vì vậy, bạn sẽ cần một bài kiểm tra "tích hợp" để kết hợp cả hai để đảm bảo các bộ phận thực sự hoạt động cùng nhau. Điều gì đã đạt được ngoài việc làm cho mọi thứ phức tạp hơn?

Ý tưởng rằng các bài kiểm tra đơn vị nên kiểm tra các phần mã của bạn tách biệt với các phần khác là sai lầm. "Đơn vị" trong một bài kiểm tra đơn vị là một đơn vị thực hiện riêng biệt. Nếu hai bài kiểm tra có thể được chạy đồng thời mà không ảnh hưởng đến nhau, thì chúng sẽ chạy riêng rẽ và các bài kiểm tra đơn vị cũng vậy. Các hàm tĩnh nhanh, không có thiết lập phức tạp và không có tác dụng phụ như ví dụ của bạn là tốt để sử dụng trực tiếp trong các thử nghiệm đơn vị. Nếu bạn có mã chậm, phức tạp để thiết lập hoặc có tác dụng phụ, thì giả là hữu ích. Họ nên tránh ở nơi khác mặc dù.


2
Bạn đưa ra một lập luận khá tốt cho sự phát triển "thử nghiệm đầu tiên". Mocking tồn tại một phần vì mã được viết theo cách quá khó để kiểm tra và nếu bạn viết bài kiểm tra của mình trước, bạn buộc mình phải viết mã dễ kiểm tra hơn.
Robert Harvey

3
Tôi hoàn toàn ủng hộ việc khuyến nghị không chế nhạo các chức năng thuần túy. Nhìn thấy điều đó quá nhiều lần.
Jared Smith

2

Sử dụng các hàm miễn phí trong C ++, kết quả sẽ giống nhau: các thử nghiệm mà các phương thức kiểm tra sử dụng các hàm miễn phí sẽ phụ thuộc vào sự thành công của các thử nghiệm chức năng miễn phí.

Làm thế nào tôi có thể xử lý các tình huống như vậy? Tôi có thể bằng cách nào đó giải quyết các phụ thuộc kiểm tra? Có cách nào tốt hơn để kiểm tra các đoạn mã ở trên không?

Chà, tôi không thấy sự phụ thuộc ở đây. Ít nhất không phải là loại buộc chúng ta phải thực hiện một thử nghiệm trước một thử nghiệm khác. Sự phụ thuộc mà chúng tôi xây dựng giữa các bài kiểm tra (bất kể loại nào) là một sự tự tin .

Chúng tôi xây dựng một đoạn mã (thử nghiệm đầu tiên hoặc không) và chúng tôi đảm bảo các thử nghiệm vượt qua. Sau đó, chúng tôi đang ở một vị trí xây dựng nhiều mã hơn. Tất cả các mã được xây dựng dựa trên điều này đầu tiên được xây dựng dựa trên sự tự tin và chắc chắn. Đây là ít nhiều những gì @DavidArno giải thích (rất tốt) trong câu trả lời của ông.

Đúng. Đó là lý do tại sao bạn có các bài kiểm tra cho X. Nếu các bài kiểm tra đó vượt qua, hàm sẽ đáp ứng thông số kỹ thuật được xác định trong các bài kiểm tra đó. Vì vậy, Y có thể gọi nó một cách an toàn và không lo lắng về nó. Nó đã được bảo hiểm rồi.

Các bài kiểm tra đơn vị nên chạy theo mọi thứ tự, mọi lúc, mọi môi trường và nhanh nhất có thể. Cho dù TestToHex_Worksđược thực hiện đầu tiên hay cuối cùng không nên làm bạn lo lắng.

Nếu TestToHex_Worksthất bại do lỗi trong ToHex, tất cả các thử nghiệm dựa trên ToHexsẽ kết thúc với các kết quả khác nhau và thất bại (lý tưởng). Chìa khóa ở đây là phát hiện những kết quả khác nhau. Chúng tôi làm nó làm cho các bài kiểm tra đơn vị được xác định. Như bạn làm ở đây

var bytes = new byte[] { 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef };
Assert.AreEqual("0123456789abcdef", bytes.ToHex());

Kiểm tra đơn vị hơn nữa dựa trên ToHexcũng nên được xác định. Nếu mọi thứ ToHexsuôn sẻ, kết quả sẽ là kết quả mong đợi. Nếu bạn nhận được một cái khác, một cái gì đó đã sai, ở đâu đó và đây là những gì bạn muốn từ một bài kiểm tra đơn vị, để phát hiện những thay đổi tinh tế này và thất bại nhanh chóng.


1

Có một chút khó khăn với một ví dụ tối thiểu như vậy, nhưng hãy xem xét lại những gì chúng ta đang làm:

Giả sử tôi đã viết một phương thức mở rộng trong c # cho các mảng byte mã hóa chúng thành các chuỗi hex, như sau:

Tại sao bạn viết một phương pháp mở rộng để làm điều này? Trừ khi bạn viết thư viện để mã hóa các mảng byte thành chuỗi, đây không phải là yêu cầu bạn đang cố gắng thực hiện. Có vẻ như bạn đang làm ở đây đã cố gắng thực hiện yêu cầu "Xác thực một số tải trọng là một mảng byte" - vì vậy các bài kiểm tra đơn vị bạn đang thực hiện phải là "Đưa ra một tải trọng hợp lệ X, phương thức của tôi trả về đúng" và "Được cung cấp đúng một tải trọng không hợp lệ Y, phương thức của tôi trả về false ".

Vì vậy, khi bạn thực hiện điều này ban đầu, bạn có thể thực hiện nội tuyến "byte-to-hex" trong phương thức. Và điều đó tốt. Sau đó, bạn nhận được một số yêu cầu khác (ví dụ: "Hiển thị tải trọng dưới dạng chuỗi hex"), trong khi bạn thực hiện nó, bạn nhận ra cũng yêu cầu bạn chuyển đổi một mảng byte thành chuỗi hex. Tại thời điểm đó, bạn tạo phương thức mở rộng của mình, cấu trúc lại phương thức cũ và gọi nó từ mã mới của bạn. Các thử nghiệm của bạn cho chức năng xác nhận không nên thay đổi. Các thử nghiệm của bạn để hiển thị tải trọng phải giống nhau cho dù mã byte-hex là nội tuyến hay trong một phương thức tĩnh. Phương thức tĩnh là một chi tiết triển khai bạn không nên quan tâm khi viết bài kiểm tra. Mã trong phương thức tĩnh sẽ được kiểm tra bởi người tiêu dùng của nó. Tôi không'


"đây không phải là yêu cầu bạn đang cố gắng thực hiện. Có vẻ như bạn đang làm ở đây đang cố gắng thực hiện yêu cầu" - bạn đang đưa ra rất nhiều giả định về mục đích của một chức năng rất có thể chỉ được chọn làm ví dụ đại diện, và không phải là một chương trình lớn hơn.
tên

"Đơn vị" trong "kiểm thử đơn vị" không có nghĩa là một chức năng hoặc một lớp. Nó có nghĩa là một đơn vị chức năng. Trừ khi bạn đặc biệt viết một thư viện chuyển đổi byte thành chuỗi, phần đó là một chi tiết thực hiện của một số hành vi hữu ích hơn. Hành vi hữu ích đó là phần bạn muốn thực hiện các bài kiểm tra, vì đó là phần bạn quan tâm về việc chính xác. Trong một thế giới lý tưởng, bạn muốn có thể tìm thấy một thư viện đã tồn tại để thực hiện bin-to-hex (hoặc bất kể đó là gì, đừng gác máy về các chi tiết cụ thể), trao đổi nó để thực hiện và không thay đổi bất kỳ xét nghiệm.
Chris Cooper

Ma quỷ là trong các chi tiết. Kiểm tra đơn vị "chi tiết thực hiện" vẫn là một điều rất hữu ích để làm và không phải là một cái gì đó để lướt qua.
tên gì

0

Chính xác. Và đây là một trong những vấn đề với các phương thức tĩnh, một vấn đề khác là OOP là sự thay thế tốt hơn nhiều trong hầu hết các tình huống.

Đây cũng là một trong những lý do Dependency Injection được sử dụng.

Trong trường hợp của bạn, bạn có thể thích có một bộ chuyển đổi cụ thể mà bạn đưa vào một lớp cần chuyển đổi một số giá trị thành thập lục phân. Một giao diện , được thực hiện bởi lớp cụ thể này, sẽ xác định hợp đồng cần thiết để chuyển đổi các giá trị.

Bạn không chỉ có thể kiểm tra mã của mình dễ dàng hơn mà còn có thể trao đổi các triển khai sau này (vì có rất nhiều cách khác để chuyển đổi giá trị thành thập lục phân, một số trong số chúng tạo ra các đầu ra khác nhau).


2
Làm thế nào mà không làm cho mã phụ thuộc vào sự thành công của các thử nghiệm chuyển đổi? Việc tiêm một lớp, một giao diện hoặc bất cứ thứ gì bạn muốn không làm cho nó hoạt động một cách kỳ diệu. Nếu bạn đang nói về tác dụng phụ thì có, sẽ có hàng tấn giá trị tiêm giả thay vì mã hóa cứng khi thực hiện, nhưng vấn đề đó chưa bao giờ tồn tại ở nơi đầu tiên.
Steve Chamaillard

1
@ArseniMourzenko vì vậy khi tiêm một lớp máy tính thực hiện một multiplyphương thức, bạn có chế giễu điều này không? Có nghĩa là để tái cấu trúc (và sử dụng toán tử * thay thế), bạn sẽ phải thay đổi mọi khai báo giả trong mã của mình? Không giống như mã dễ dàng với tôi. Tất cả những gì tôi nói là toHexrất đơn giản và hoàn toàn không có tác dụng phụ vì vậy tôi không thấy bất kỳ lý do nào để chế giễu hay sơ khai điều này. Đó là quá nhiều chi phí cho hoàn toàn không có lợi nhuận. Không nói rằng chúng ta không nên tiêm nó, nhưng điều đó phụ thuộc vào cách sử dụng.
Steve Chamaillard

2
@Ewan, nếu tôi sửa đổi một đoạn mã và sau đó đối mặt với " biển đỏ ", tôi có thể nghĩ ồ không, tôi phải bắt đầu từ đâu?. Nhưng có lẽ tôi chỉ có thể quay lại đoạn mã tôi vừa sửa đổi và xem xét các thử nghiệm của nó trước để xem những gì tôi đã phá vỡ. : p
David Arno

3
@ArseniMourzenko, nếu DI làm cho thiết kế của bạn tốt hơn nhiều, tại sao tôi không thấy các đối số cho các kiểu tiêm giống như Stringinttốt? Hoặc các lớp tiện ích cơ bản từ thư viện tiêu chuẩn?
Bart van Ingen Schenau

4
Câu trả lời của bạn hoàn toàn bỏ qua các khía cạnh thực tế. Có rất nhiều tình huống với các phương thức tĩnh nhanh, khép kín và không đủ khả năng để thay đổi, đó là nỗ lực tiêm chúng hoặc nhảy qua bất kỳ vòng quay nào khác ngoài một cuộc gọi chức năng thông thường, là một sự lãng phí thời gian vô nghĩa và cố gắng.
whatsisname
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.