Có nên kiểm tra kết quả đơn vị dự kiến?


29

Các kết quả dự kiến ​​của một bài kiểm tra đơn vị sẽ được mã hóa cứng, hoặc chúng có thể phụ thuộc vào các biến khởi tạo không? Các kết quả được mã hóa cứng hoặc tính toán có làm tăng nguy cơ đưa ra các lỗi trong bài kiểm tra đơn vị không? Có những yếu tố khác tôi chưa xem xét?

Ví dụ, cái nào trong hai cái này là một định dạng đáng tin cậy hơn?

[TestMethod]
public void GetPath_Hardcoded()
{
    MyClass target = new MyClass("fields", "that later", "determine", "a folder");
    string expected = "C:\\Output Folder\\fields\\that later\\determine\\a folder";
    string actual = target.GetPath();
    Assert.AreEqual(expected, actual,
        "GetPath should return a full directory path based on its fields.");
}

[TestMethod]
public void GetPath_Softcoded()
{
    MyClass target = new MyClass("fields", "that later", "determine", "a folder");
    string expected = "C:\\Output Folder\\" + string.Join("\\", target.Field1, target.Field2, target.Field3, target.Field4);
    string actual = target.GetPath();
    Assert.AreEqual(expected, actual,
        "GetPath should return a full directory path based on its fields.");
}

EDIT 1: Đáp lại câu trả lời của DXM, tùy chọn 3 có phải là giải pháp ưa thích không?

[TestMethod]
public void GetPath_Option3()
{
    string field1 = "fields";
    string field2 = "that later";
    string field3 = "determine";
    string field4 = "a folder";
    MyClass target = new MyClass(field1, field2, field3, field4);
    string expected = "C:\\Output Folder\\" + string.Join("\\", field1, field2, field3, field4);
    string actual = target.GetPath();
    Assert.AreEqual(expected, actual,
        "GetPath should return a full directory path based on its fields.");
}

2
Làm tất cả. Nghiêm túc. Các xét nghiệm có thể và nên chồng chéo. Cũng xem xét một số loại thử nghiệm dựa trên dữ liệu nếu bạn thấy mình đang xử lý các giá trị được mã hóa cứng.
Công việc

Tôi đồng ý tùy chọn thứ ba là những gì tôi muốn sử dụng. Tôi không nghĩ rằng tùy chọn 1 sẽ bị tổn thương vì bạn loại bỏ thao tác biên dịch.
kwelch

Cả hai tùy chọn của bạn đều sử dụng mã hóa cứng và sẽ bị
hỏng

Câu trả lời:


27

Tôi nghĩ tính toán giá trị kết quả dự kiến ​​trong các trường hợp thử nghiệm mạnh mẽ và linh hoạt hơn. Ngoài ra, bằng cách sử dụng tên biến tốt trong biểu thức tính kết quả mong đợi, rõ ràng hơn là kết quả mong đợi đến từ đâu.

Như đã nói, trong ví dụ cụ thể của bạn, tôi sẽ KHÔNG tin vào phương pháp "Mã hóa mềm" bởi vì nó sử dụng SUT (hệ thống đang thử nghiệm) của bạn làm đầu vào cho các tính toán của bạn. Nếu có lỗi trong MyClass nơi các trường không được lưu trữ đúng cách, thử nghiệm của bạn sẽ thực sự vượt qua vì tính toán giá trị dự kiến ​​của bạn sẽ sử dụng chuỗi sai giống như target.GetPath ().

Đề xuất của tôi sẽ là tính toán giá trị mong đợi khi có ý nghĩa, nhưng đảm bảo rằng phép tính không phụ thuộc vào bất kỳ mã nào từ chính SUT.

Đáp lại cập nhật của OP đối với phản hồi của tôi:

Có, dựa trên kiến ​​thức của tôi nhưng kinh nghiệm có phần hạn chế khi làm TDD, tôi sẽ chọn tùy chọn # 3.


1
Điểm tốt! Đừng dựa vào đối tượng chưa được xác minh trong bài kiểm tra.
Thực phẩm điện tử

không phải là sao chép mã SUT sao?
Abyx

1
theo một cách nào đó, nhưng đó là cách bạn xác minh rằng SUT đang hoạt động. Nếu chúng tôi sử dụng cùng một mã và nó đã bị vỡ, bạn sẽ không bao giờ biết. Tất nhiên, nếu để thực hiện phép tính, bạn cần nhân đôi nhiều SUT, thì có thể tùy chọn # 1 sẽ trở nên tốt hơn, chỉ cần mã hóa giá trị.
DXM

16

Điều gì xảy ra nếu mã như sau:

MyTarget() // constructor
{
   Field1 = Field2 = Field3 = Field4 = "";
}

Ví dụ thứ hai của bạn sẽ không bắt được lỗi, nhưng ví dụ đầu tiên thì có.

Nói chung, tôi khuyên bạn nên chống lại mã hóa mềm vì nó có thể che giấu các lỗi. Ví dụ:

string expected = "C:\\Output Folder" + string.Join("\\", target.Field1, target.Field2, target.Field3, target.Field4);

Bạn có thể phát hiện ra vấn đề? Bạn sẽ không phạm sai lầm tương tự trong phiên bản mã hóa cứng. Khó có thể tính toán chính xác hơn các giá trị được mã hóa cứng. Đó là lý do tại sao tôi thích làm việc với các giá trị được mã hóa cứng hơn các giá trị được mã hóa mềm.

Nhưng có những trường hợp ngoại lệ. Nếu mã của bạn phải chạy trên Windows và Linux thì sao? Đường dẫn không chỉ phải khác nhau, mà còn phải sử dụng các đường phân cách khác nhau! Tính toán đường dẫn sử dụng các hàm trừu tượng sự khác biệt giữa có thể có ý nghĩa trong bối cảnh đó.


Tôi nghe thấy những gì bạn nói và điều đó đôi khi cho tôi xem xét. Mã hóa mềm phụ thuộc vào các trường hợp thử nghiệm khác của tôi (chẳng hạn như ConstructorShouldC CorrlyInitialiseFields) đi qua. Thất bại mà bạn mô tả sẽ được tham chiếu chéo bởi các bài kiểm tra đơn vị khác không thành công.
Thực phẩm điện tử cầm tay

@ Hand-E-Food, có vẻ như bạn đang viết bài kiểm tra về các phương pháp riêng lẻ của các đối tượng của bạn. Đừng. Bạn nên viết các bài kiểm tra kiểm tra tính chính xác của toàn bộ đối tượng của bạn với nhau chứ không phải các phương thức riêng lẻ. Nếu không, các bài kiểm tra của bạn sẽ dễ vỡ đối với những thay đổi bên trong đối tượng.
Winston Ewert

Tôi không chắc là tôi làm theo. Ví dụ tôi đưa ra hoàn toàn là giả thuyết, một kịch bản dễ hiểu. Tôi đang viết bài kiểm tra đơn vị để kiểm tra các thành viên công khai của các lớp và đối tượng. Đó có phải là cách chính xác để sử dụng chúng?
Thực phẩm điện tử

@ Hand-E-Food, nếu tôi hiểu đúng về bạn, bài kiểm tra của bạn ConstructShouldC CorrlyInitialiseFields sẽ gọi hàm tạo và sau đó xác nhận rằng các trường được đặt chính xác. Nhưng bạn không nên làm điều đó. Bạn không nên quan tâm những gì các lĩnh vực nội bộ đang làm. Bạn chỉ nên khẳng định rằng hành vi bên ngoài của đối tượng là chính xác. Nếu không, ngày có thể đến khi bạn cần thay thế việc thực hiện nội bộ. Nếu bạn đã xác nhận về trạng thái nội bộ, tất cả các bài kiểm tra của bạn sẽ bị hỏng. Nhưng nếu bạn chỉ xác nhận về hành vi bên ngoài, mọi thứ sẽ vẫn hoạt động.
Winston Ewert

@ Winston - Tôi thực sự đang trong quá trình cày xới cuốn sách Các mẫu thử nghiệm xUnit và trước khi hoàn thành Nghệ thuật kiểm tra đơn vị. Tôi sẽ không giả vờ rằng tôi biết những gì tôi đang nói, nhưng tôi muốn nghĩ rằng tôi đã nhặt được một cái gì đó từ những cuốn sách đó. Cả hai cuốn sách đều khuyến nghị rằng mỗi phương pháp kiểm tra nên kiểm tra tối thiểu tuyệt đối và bạn nên có nhiều trường hợp kiểm tra để kiểm tra toàn bộ đối tượng của mình. Theo cách đó, khi giao diện hoặc chức năng thay đổi, bạn chỉ nên sửa một vài phương thức kiểm tra, thay vì hầu hết trong số chúng. Và vì chúng nhỏ, nên thay đổi sẽ dễ dàng hơn.
DXM

4

Theo tôi, cả hai đề xuất của bạn đều ít hơn lý tưởng. Cách lý tưởng để làm điều này là cách này:

[TestMethod]
public void GetPath_Hardcoded()
{
    const string f1 = "fields"; const string f2 = "that later"; 
    const string f3 = "determine"; const string f4 = "a folder";

    MyClass target = new MyClass( f1, f2, f3, f4 );
    string expected = "C:\\Output Folder\\" + string.Join("\\", f1, f2, f3, f4);
    string actual = target.GetPath();
    Assert.AreEqual(expected, actual,
        "GetPath should return a full directory path based on its fields.");
}

Nói cách khác, kiểm tra nên hoạt động độc quyền dựa trên đầu vào và đầu ra của đối tượng, và không dựa trên trạng thái bên trong của đối tượng. Đối tượng nên được coi là một hộp đen. (Tôi bỏ qua các vấn đề khác, như sự không phù hợp của việc sử dụng chuỗi.Join thay vì Path.Combine, vì đây chỉ là một ví dụ.)


1
Không phải tất cả các phương thức đều hoạt động - nhiều chính xác có tác dụng phụ làm thay đổi trạng thái của một số đối tượng hoặc đối tượng. Một thử nghiệm đơn vị cho một phương pháp có tác dụng phụ có thể sẽ cần phải đánh giá trạng thái của (các) đối tượng bị ảnh hưởng bởi phương pháp đó.
Matthew Flynn

Sau đó, trạng thái đó sẽ được coi là đầu ra của phương thức. Mục đích của thử nghiệm mẫu này là kiểm tra phương thức GetPath (), chứ không phải hàm tạo của MyClass. Đọc câu trả lời của @ DXM, anh ta đưa ra một lý do rất chính đáng để thực hiện phương pháp hộp đen.
Mike Nakis

@MatthewFlynn, sau đó bạn nên kiểm tra các phương thức bị ảnh hưởng bởi trạng thái đó. Trạng thái nội bộ chính xác là một chi tiết triển khai và không phải là hoạt động kinh doanh của thử nghiệm.
Winston Ewert

@MatthewFlynn, chỉ cần làm rõ, có liên quan đến ví dụ được hiển thị, hoặc một cái gì đó khác để xem xét cho các bài kiểm tra đơn vị khác? Tôi có thể thấy rằng vấn đề quan trọng đối với một cái gì đó như target.Dispose(); Assert.IsTrue(target.IsDisposed);(một ví dụ rất đơn giản.)
Thực phẩm điện tử

Ngay cả trong trường hợp này, thuộc tính IsDisposed là (hoặc nên) là một phần không thể thiếu trong giao diện chung của lớp và không phải là một chi tiết triển khai. (Giao diện IDispose không cung cấp một tài sản như vậy, nhưng điều đó thật đáng tiếc.)
Mike Nakis

2

Có hai khía cạnh trong cuộc thảo luận:

1. Sử dụng chính mục tiêu cho trường hợp thử nghiệm
Câu hỏi đầu tiên là bạn có nên / có thể sử dụng chính lớp đó để dựa và nhận một phần công việc được thực hiện trong cuống thử không? - Câu trả lời là KHÔNG vì nói chung, bạn không bao giờ nên đưa ra giả định về mã mà bạn đang kiểm tra. Nếu điều này không được thực hiện đúng cách, các lỗi theo thời gian sẽ trở nên miễn nhiễm với một số thử nghiệm đơn vị.

2. Mã hóa cứng
bạn nên mã cứng ? Một lần nữa câu trả lời là Không . bởi vì giống như bất kỳ phần mềm nào - việc mã hóa cứng thông tin của anh ta trở nên khó khăn khi mọi thứ phát triển. Ví dụ, khi bạn muốn sửa đổi đường dẫn trên, bạn cần viết đơn vị bổ sung hoặc tiếp tục sửa đổi. Một phương pháp tốt hơn là giữ ngày đầu vào và ngày đánh giá xuất phát từ cấu hình riêng biệt có thể dễ dàng điều chỉnh.

ví dụ ở đây là cách tôi sẽ kiểm tra sơ khai.

[TestMethod]
public void GetPath_Tested(int CaseId)
{
    testParams = GetTestConfig(caseID,"testConfig.txt"); // some wrapper that does read line and chops the field. 
    MyClass target = new MyClass(testParams.field1, testParams.field2);
    string expected = testParams.field5;
    string actual = target.GetPath();
    Assert.AreEqual(expected, actual,
        "GetPath should return a full directory path based on its fields.");
}

0

Có rất nhiều khái niệm có thể, làm một số ví dụ để thấy sự khác biệt

[TestMethod]
public void GetPath_Softcoded()
{
    //Hardcoded since you want to see what you expect is most simple and clear
    string expected = "C:\\Output Folder\\fields\\that later\\determine\\a folder";

    //If this test should also use a mocked filesystem it might be that you want to use
    //some base directory, which you could set in the setUp of your test class
    //that is usefull if you you need to run the same test on different environments
    string expected = this.outputPath + "fields\\that later\\determine\\a folder";


    //another readable way could be interesting if you have difficult variables needed to test
    string fields = "fields";
    string thatLater = "that later";
    string determine = "determine";
    string aFolder = "a folder";
    string expected = this.outputPath + fields + "\\" + thatLater + "\\" + determine + "\\" + aFolder;
    MyClass target = new MyClass(fields, thatLater, determine, aFolder);

    //in general testing with real words is not needed, so code could be shorter on that
    //for testing difficult folder names you write a separate test anyway
    string f1 = "f1";
    string f2 = "f2";
    string f3 = "f3";
    string f4 = "f4";
    string expected = this.outputPath + f1 + "\\" + f2 + "\\" + f3 + "\\" + f4;
    MyClass target = new MyClass(f1, f2, f3, f4);

    //so here we start to see a structure, it looks more like an array of fields
    //so what would make testing more interesting with lots of variables is the use of a data provider
    //the data provider will re-use your test with many different kinds of inputs. That will reduce the amount of duplication of code for testing
    //http://msdn.microsoft.com/en-us/library/ms182527.aspx


    The part where you compare already seems correct
    MyClass target = new MyClass(fields, thatLater, determine, aFolder);

    string actual = target.GetPath();
    Assert.AreEqual(expected, actual,
        "GetPath should return a full directory path based on its fields.");
}

Tóm lại: Nói chung, bài kiểm tra mã hóa cứng đầu tiên của bạn có ý nghĩa nhất đối với tôi bởi vì nó đơn giản, đi thẳng vào vấn đề, v.v ... Nếu bạn bắt đầu mã hóa một đường dẫn quá nhiều lần, hãy đặt nó vào phương thức thiết lập.

Để kiểm tra cấu trúc trong tương lai nhiều hơn, tôi sẽ kiểm tra các nguồn dữ liệu để bạn có thể thêm nhiều hàng dữ liệu hơn nếu bạn cần thêm các tình huống kiểm tra.


0

Các khung kiểm tra hiện đại cho phép bạn cung cấp các tham số cho phương thức của mình. Tôi tận dụng những thứ đó:

[TestCase("fields", "that later", "determine", "a folder", @"C:\Output Folder\fields\that later\determine\a folder")]
public void GetPathShouldReturnFullDirectoryPathBasedOnItsFields(
    string field1, string field2, string field3, string field,
    string expected)
{
    MyClass target = new MyClass(field1, field2, field3, field4);
    string actual = target.GetPath();
    Assert.AreEqual(expected, actual,
        "GetPath should return a full directory path based on its fields.");
}

Có một số lợi thế cho điều này, theo quan điểm của tôi:

  1. Các nhà phát triển thường bị cám dỗ sao chép các phần mã có vẻ đơn giản từ SUT của họ vào các bài kiểm tra đơn vị của họ. Như Winston chỉ ra , những người đó vẫn có thể có những lỗi khó khăn ẩn giấu trong đó. "Mã hóa cứng" kết quả mong đợi sẽ giúp tránh các tình huống trong đó mã kiểm tra của bạn không chính xác vì cùng lý do mã gốc của bạn không chính xác. Nhưng nếu một sự thay đổi trong các yêu cầu buộc bạn phải theo dõi các chuỗi mã hóa cứng được nhúng bên trong hàng tá phương thức thử nghiệm, điều đó có thể gây khó chịu. Có tất cả các giá trị được mã hóa cứng ở một nơi, bên ngoài logic thử nghiệm của bạn, mang đến cho bạn những điều tốt nhất của cả hai thế giới.
  2. Bạn có thể thêm các thử nghiệm cho các đầu vào khác nhau và đầu ra dự kiến ​​với một dòng mã. Điều này khuyến khích bạn viết nhiều bài kiểm tra hơn, trong khi vẫn giữ mã kiểm tra DRY và dễ bảo trì. Tôi thấy rằng vì quá rẻ để thêm các bài kiểm tra, tâm trí của tôi được mở ra cho các trường hợp thử nghiệm mới mà tôi không nghĩ tới nếu tôi phải viết một phương pháp hoàn toàn mới cho chúng. Ví dụ, hành vi nào tôi mong đợi nếu một trong các đầu vào có dấu chấm trong đó? Một dấu gạch chéo ngược? Điều gì nếu một người trống rỗng? Hay khoảng trắng? Hoặc bắt đầu hoặc kết thúc với khoảng trắng?
  3. Khung thử nghiệm sẽ coi mỗi TestCase là thử nghiệm riêng của mình, thậm chí đặt các đầu vào và đầu ra được cung cấp vào tên thử nghiệm. Nếu tất cả các TestCase vượt qua nhưng một, thì thật dễ dàng để xem cái nào đã phá vỡ và nó khác với tất cả những cái khác như thế nào.
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.