it-swarm-vi.com

Có thể có nhiều khẳng định trong một bài kiểm tra đơn vị không?

Trong nhận xét cho bài đăng tuyệt vời này , Roy Osherove đã đề cập đến dự án OAPT được thiết kế để chạy từng khẳng định trong một thử nghiệm.

Sau đây được viết trên trang chủ của dự án:

Các bài kiểm tra đơn vị thích hợp sẽ thất bại vì chính xác một lý do, rằng Lít tại sao bạn nên sử dụng một xác nhận cho mỗi bài kiểm tra đơn vị.

Và, cũng, Roy đã viết trong các bình luận:

Hướng dẫn của tôi thường là bạn kiểm tra một CONCEPT logic cho mỗi bài kiểm tra. bạn có thể có nhiều xác nhận trên cùng một object. chúng thường sẽ là cùng một khái niệm đang được thử nghiệm.

Tôi nghĩ rằng, có một số trường hợp cần nhiều xác nhận (ví dụ Xác nhận bảo vệ ), nhưng nói chung tôi cố gắng tránh điều này. Ý kiến ​​của bạn là gì? Vui lòng cung cấp một ví dụ trong thế giới thực trong đó nhiều xác nhận thực sự cần thiết.

430
Restuta

Tôi không nghĩ rằng đó nhất thiết là một điều xấu , nhưng tôi nghĩ chúng ta nên cố gắng chỉ có những khẳng định duy nhất trong các thử nghiệm của chúng tôi. Điều này có nghĩa là bạn viết nhiều bài kiểm tra hơn và các bài kiểm tra của chúng tôi sẽ chỉ kiểm tra một điều duy nhất tại một thời điểm.

Có nói rằng, tôi sẽ nói có thể một nửa các bài kiểm tra của tôi thực sự chỉ có một khẳng định. Tôi nghĩ rằng nó chỉ trở thành một mã (thử nghiệm?) Mùi khi bạn có khoảng năm xác nhận trở lên trong thử nghiệm của mình.

Làm thế nào để bạn giải quyết nhiều khẳng định?

246
Jaco Pretorius

Các thử nghiệm chỉ thất bại vì một lý do duy nhất, nhưng điều đó không phải lúc nào cũng có nghĩa là chỉ nên có một câu lệnh Assert. IMHO điều quan trọng hơn là phải giữ mẫu " Sắp xếp, Act, Khẳng định ".

Điều quan trọng là bạn chỉ có một hành động, và sau đó bạn kiểm tra kết quả của hành động đó bằng cách sử dụng các xác nhận. Nhưng đó là "Sắp xếp, Hành động, Khẳng định, Kết thúc kiểm tra". Nếu bạn muốn tiếp tục thử nghiệm bằng cách thực hiện một hành động khác và nhiều xác nhận sau đó, hãy thực hiện thử nghiệm riêng thay thế.

Tôi rất vui khi thấy nhiều tuyên bố khẳng định tạo thành các phần của thử nghiệm cùng một hành động. ví dụ.

[Test]
public void ValueIsInRange()
{
  int value = GetValueToTest();

  Assert.That(value, Is.GreaterThan(10), "value is too small");
  Assert.That(value, Is.LessThan(100), "value is too large");
} 

hoặc là

[Test]
public void ListContainsOneValue()
{
  var list = GetListOf(1);

  Assert.That(list, Is.Not.Null, "List is null");
  Assert.That(list.Count, Is.EqualTo(1), "Should have one item in list");
  Assert.That(list[0], Is.Not.Null, "Item is null");
} 

Bạn could kết hợp những điều này thành một khẳng định, nhưng đó là một điều khác với việc khăng khăng rằng bạn nên hoặc phải. Không có cải tiến từ việc kết hợp chúng.

ví dụ. Cái đầu tiên could được

Assert.IsTrue((10 < value) && (value < 100), "Value out of range"); 

Nhưng điều này không tốt hơn - thông báo lỗi từ nó ít cụ thể hơn và nó không có lợi thế nào khác. Tôi chắc rằng bạn có thể nghĩ về các ví dụ khác khi kết hợp hai hoặc ba (hoặc nhiều hơn) khẳng định vào một điều kiện boolean lớn làm cho nó khó đọc hơn, khó thay đổi hơn và khó tìm ra lý do tại sao nó thất bại. Tại sao làm điều này chỉ vì lợi ích của một quy tắc?

[~ # ~] nb [~ # ~] : Mã mà tôi đang viết ở đây là C # với NUnit, nhưng các nguyên tắc sẽ tuân theo các ngôn ngữ khác và khung. Cú pháp có thể rất giống nhau quá.

314
Anthony

Tôi chưa bao giờ nghĩ rằng nhiều hơn một khẳng định là một điều xấu.

Tôi làm nó suốt:

public void ToPredicateTest()
{
    ResultField rf = new ResultField(ResultFieldType.Measurement, "name", 100);
    Predicate<ResultField> p = (new ConditionBuilder()).LessThanConst(400)
                                                       .Or()
                                                       .OpenParenthesis()
                                                       .GreaterThanConst(500)
                                                       .And()
                                                       .LessThanConst(1000)
                                                       .And().Not()
                                                       .EqualsConst(666)
                                                       .CloseParenthesis()
                                                       .ToPredicate();
    Assert.IsTrue(p(ResultField.FillResult(rf, 399)));
    Assert.IsTrue(p(ResultField.FillResult(rf, 567)));
    Assert.IsFalse(p(ResultField.FillResult(rf, 400)));
    Assert.IsFalse(p(ResultField.FillResult(rf, 666)));
    Assert.IsFalse(p(ResultField.FillResult(rf, 1001)));

    Predicate<ResultField> p2 = (new ConditionBuilder()).EqualsConst(true).ToPredicate();

    Assert.IsTrue(p2(new ResultField(ResultFieldType.Confirmation, "Is True", true)));
    Assert.IsFalse(p2(new ResultField(ResultFieldType.Confirmation, "Is False", false)));
}

Ở đây tôi sử dụng nhiều khẳng định để đảm bảo các điều kiện phức tạp có thể được biến thành vị từ dự kiến.

Tôi chỉ đang thử nghiệm một đơn vị (phương thức ToPredicate), nhưng tôi đang trình bày mọi thứ tôi có thể nghĩ đến trong thử nghiệm.

85
Matt Ellen

Khi tôi đang sử dụng thử nghiệm đơn vị để xác thực hành vi cấp cao, tôi hoàn toàn đặt nhiều xác nhận vào một thử nghiệm. Đây là một thử nghiệm tôi thực sự đang sử dụng cho một số mã thông báo khẩn cấp. Mã chạy trước khi kiểm tra đặt hệ thống vào trạng thái nếu bộ xử lý chính được chạy, một cảnh báo sẽ được gửi.

@Test
public void testAlarmSent() {
    assertAllUnitsAvailable();
    assertNewAlarmMessages(0);

    pulseMainProcessor();

    assertAllUnitsAlerting();
    assertAllNotificationsSent();
    assertAllNotificationsUnclosed();
    assertNewAlarmMessages(1);
}

Nó đại diện cho các điều kiện cần tồn tại ở mọi bước trong quy trình để tôi tự tin rằng mã đang hành xử theo cách tôi mong đợi. Nếu một xác nhận duy nhất thất bại, tôi không quan tâm rằng những cái còn lại thậm chí sẽ không được chạy; bởi vì trạng thái của hệ thống không còn hiệu lực, những xác nhận tiếp theo đó sẽ không cho tôi biết bất cứ điều gì có giá trị. * Nếu assertAllUnitsAlerting() không thành công, thì tôi sẽ không biết phải làm gì với assertAllNotificationSent() 's thành công OR thất bại cho đến khi tôi xác định được nguyên nhân gây ra lỗi trước đó và đã sửa nó.

(* - Được rồi, chúng có thể có thể hiểu được hữu ích trong việc gỡ lỗi vấn đề. Nhưng thông tin quan trọng nhất, rằng thử nghiệm thất bại, đã được nhận.)

21
BlairHippo

Một lý do khác khiến tôi nghĩ rằng, nhiều khẳng định trong một phương thức không phải là một điều xấu được mô tả trong đoạn mã sau:

class Service {
    Result process();
}

class Result {
    Inner inner;
}

class Inner {
    int number;
}

Trong thử nghiệm của tôi, tôi chỉ muốn kiểm tra rằng service.process() trả về số chính xác trong các thể hiện của lớp Inner.

Thay vì thử nghiệm ...

@Test
public void test() {
    Result res = service.process();
    if ( res != null && res.getInner() != null ) Assert.assertEquals( ..., res.getInner() );
}

Tôi đang làm

@Test
public void test() {
    Result res = service.process();
    Assert.notNull(res);
    Assert.notNull(res.getInner());
    Assert.assertEquals( ..., res.getInner() );
}
8
Betlista

Tôi nghĩ rằng có rất nhiều trường hợp viết nhiều khẳng định là hợp lệ trong quy tắc rằng một bài kiểm tra chỉ nên thất bại vì một lý do.

Ví dụ, hãy tưởng tượng một hàm phân tích chuỗi ngày:

function testParseValidDateYMD() {
    var date = Date.parse("2016-01-02");

    Assert.That(date.Year).Equals(2016);
    Assert.That(date.Month).Equals(1);
    Assert.That(date.Day).Equals(0);
}

Nếu thử nghiệm thất bại là vì một lý do, phân tích cú pháp là không chính xác. Nếu bạn cho rằng thử nghiệm này có thể thất bại vì ba lý do khác nhau, thì IMHO sẽ quá tốt trong định nghĩa của bạn về "một lý do".

6
Pete

Tôi không biết về bất kỳ tình huống nào sẽ là một ý tưởng tốt khi có nhiều xác nhận bên trong phương thức [Thử nghiệm]. Lý do chính khiến mọi người muốn có nhiều Xác nhận là vì họ đang cố gắng có một lớp [TestFixture] cho mỗi lớp được kiểm tra. Thay vào đó, bạn có thể chia các bài kiểm tra của mình thành nhiều lớp [TestFixture] hơn. Điều này cho phép bạn xem nhiều cách mà mã có thể không phản ứng theo cách bạn mong đợi, thay vì chỉ một cách mà xác nhận đầu tiên thất bại. Cách bạn đạt được điều này là bạn có ít nhất một thư mục cho mỗi lớp đang được kiểm tra với rất nhiều lớp [TestFixture] bên trong. Mỗi lớp [TestFixture] sẽ được đặt tên theo trạng thái cụ thể của một đối tượng bạn sẽ kiểm tra. Phương thức [SetUp] sẽ đưa đối tượng vào trạng thái được mô tả bằng tên lớp. Sau đó, bạn có nhiều phương thức [Kiểm tra], mỗi phương thức khẳng định những điều khác nhau mà bạn mong đợi là đúng, với trạng thái hiện tại của đối tượng. Mỗi phương thức [Kiểm tra] được đặt tên theo điều mà nó đang khẳng định, ngoại trừ có lẽ nó có thể được đặt tên theo khái niệm thay vì chỉ đọc mã tiếng Anh. Sau đó, mỗi triển khai phương thức [Kiểm tra] chỉ cần một dòng mã trong đó nó đang xác nhận một cái gì đó. Một ưu điểm khác của phương pháp này là nó làm cho các bài kiểm tra rất dễ đọc vì nó trở nên khá rõ ràng những gì bạn đang kiểm tra và những gì bạn mong đợi chỉ bằng cách nhìn vào tên lớp và phương thức. Điều này cũng sẽ mở rộng tốt hơn khi bạn bắt đầu nhận ra tất cả các trường hợp Edge nhỏ mà bạn muốn kiểm tra và khi bạn tìm thấy lỗi.

Thông thường, điều này có nghĩa là dòng mã cuối cùng bên trong phương thức [SetUp] sẽ lưu trữ giá trị thuộc tính hoặc giá trị trả về trong một biến đối tượng riêng của [TestFixture]. Sau đó, bạn có thể xác nhận nhiều điều khác nhau về biến thể hiện này từ các phương thức [Kiểm tra] khác nhau. Bạn cũng có thể đưa ra các xác nhận về các thuộc tính khác nhau của đối tượng được kiểm tra được đặt thành bây giờ nó ở trạng thái mong muốn.

Đôi khi bạn cần phải xác nhận trên đường đi khi bạn đang đưa đối tượng được kiểm tra vào trạng thái mong muốn để đảm bảo bạn không làm phiền trước khi đưa đối tượng vào trạng thái mong muốn. Trong trường hợp đó, các xác nhận bổ sung đó sẽ xuất hiện bên trong phương thức [SetUp]. Nếu có lỗi xảy ra trong phương thức [SetUp], thì rõ ràng có điều gì đó không ổn với thử nghiệm trước khi đối tượng rơi vào trạng thái mong muốn mà bạn dự định kiểm tra.

Một vấn đề khác bạn có thể gặp phải là bạn có thể đang kiểm tra Ngoại lệ mà bạn dự kiến ​​sẽ bị ném. Điều này có thể cám dỗ bạn không theo mô hình trên. Tuy nhiên, vẫn có thể đạt được bằng cách bắt ngoại lệ bên trong phương thức [SetUp] và lưu trữ nó vào một biến thể hiện. Điều này sẽ cho phép bạn khẳng định những điều khác nhau về ngoại lệ, mỗi phương thức [Thử nghiệm] của riêng nó. Sau đó, bạn cũng có thể xác nhận những điều khác về đối tượng được thử nghiệm để đảm bảo không có tác dụng phụ ngoài ý muốn từ ngoại lệ bị ném.

Ví dụ (điều này sẽ được chia thành nhiều tệp):

namespace Tests.AcctTests
{
    [TestFixture]
    public class no_events
    {
        private Acct _acct;

        [SetUp]
        public void SetUp() {
            _acct = new Acct();
        }

        [Test]
        public void balance_0() {
            Assert.That(_acct.Balance, Is.EqualTo(0m));
        }
    }

    [TestFixture]
    public class try_withdraw_0
    {
        private Acct _acct;
        private List<string> _problems;

        [SetUp]
        public void SetUp() {
            _acct = new Acct();
            Assert.That(_acct.Balance, Is.EqualTo(0));
            _problems = _acct.Withdraw(0m);
        }

        [Test]
        public void has_problem() {
            Assert.That(_problems, Is.EquivalentTo(new string[] { "Withdraw amount must be greater than zero." }));
        }

        [Test]
        public void balance_not_changed() {
            Assert.That(_acct.Balance, Is.EqualTo(0m));
        }
    }

    [TestFixture]
    public class try_withdraw_negative
    {
        private Acct _acct;
        private List<string> _problems;

        [SetUp]
        public void SetUp() {
            _acct = new Acct();
            Assert.That(_acct.Balance, Is.EqualTo(0));
            _problems = _acct.Withdraw(-0.01m);
        }

        [Test]
        public void has_problem() {
            Assert.That(_problems, Is.EquivalentTo(new string[] { "Withdraw amount must be greater than zero." }));
        }

        [Test]
        public void balance_not_changed() {
            Assert.That(_acct.Balance, Is.EqualTo(0m));
        }
    }
}
3
still_dreaming_1

Nếu bạn có nhiều xác nhận trong một chức năng kiểm tra, tôi hy vọng chúng có liên quan trực tiếp đến thử nghiệm bạn đang tiến hành. Ví dụ,

@Test
test_Is_Date_segments_correct {

   // It is okay if you have multiple asserts checking dd, mm, yyyy, hh, mm, ss, etc. 
   // But you would not have any assert statement checking if it is string or number,
   // that is a different test and may be with multiple or single assert statement.
}

Có rất nhiều bài kiểm tra (ngay cả khi bạn cảm thấy rằng nó có thể là quá mức cần thiết) không phải là một điều xấu. Bạn có thể lập luận rằng có các bài kiểm tra quan trọng và thiết yếu nhất là quan trọng hơn. Vì vậy, khi bạn đang khẳng định, hãy chắc chắn rằng các câu khẳng định của bạn được đặt chính xác thay vì lo lắng về nhiều khẳng định quá nhiều. Nếu bạn cần nhiều hơn một, sử dụng nhiều hơn một.

2
hagubear

Có nhiều xác nhận trong cùng một bài kiểm tra chỉ là vấn đề khi bài kiểm tra thất bại. Sau đó, bạn có thể phải gỡ lỗi bài kiểm tra hoặc phân tích ngoại lệ để tìm ra khẳng định nào không thành công. Với một khẳng định trong mỗi bài kiểm tra, thường dễ dàng xác định những gì sai.

Tôi không thể nghĩ ra một kịch bản trong đó nhiều xác nhận thực sự cần thiết, vì bạn luôn có thể viết lại chúng dưới dạng nhiều điều kiện trong cùng một xác nhận. Tuy nhiên, có thể thích hợp hơn nếu bạn có một số bước để xác minh dữ liệu trung gian giữa các bước thay vì mạo hiểm rằng các bước sau đó bị sập vì đầu vào xấu.

2
Guffa

Nếu thử nghiệm của bạn thất bại, bạn sẽ không biết liệu các xác nhận sau đây cũng sẽ bị hỏng hay không. Thông thường, điều đó có nghĩa là bạn sẽ thiếu thông tin có giá trị để tìm ra nguồn gốc của vấn đề. Giải pháp của tôi là sử dụng một khẳng định nhưng với một vài giá trị:

String actual = "val1="+val1+"\nval2="+val2;
assertEquals(
    "val1=5\n" +
    "val2=hello"
    , actual
);

Điều đó cho phép tôi thấy tất cả các xác nhận thất bại cùng một lúc. Tôi sử dụng một số dòng vì hầu hết các IDE sẽ hiển thị các chuỗi khác nhau trong một hộp thoại so sánh cạnh nhau.

2
Aaron Digulla

Mục tiêu của bài kiểm tra đơn vị là cung cấp cho bạn càng nhiều thông tin càng tốt về những gì đang thất bại nhưng cũng để giúp xác định chính xác các vấn đề cơ bản nhất trước tiên. Khi bạn biết một cách hợp lý rằng một xác nhận sẽ thất bại do một xác nhận khác thất bại hoặc nói cách khác, có một mối quan hệ phụ thuộc giữa thử nghiệm, thì sẽ rất hợp lý khi đưa các xác nhận này thành nhiều xác nhận trong một thử nghiệm. Điều này có lợi ích của việc không xả rác các kết quả thử nghiệm với những thất bại rõ ràng có thể bị loại bỏ nếu chúng tôi bảo lãnh cho khẳng định đầu tiên trong một thử nghiệm. Trong trường hợp mối quan hệ này không tồn tại, thì đương nhiên sẽ tách các xác nhận này thành các thử nghiệm riêng lẻ bởi vì nếu không tìm thấy những thất bại này sẽ yêu cầu nhiều lần thử nghiệm để giải quyết tất cả các vấn đề.

Nếu sau đó bạn cũng thiết kế các đơn vị/lớp theo cách mà các bài kiểm tra quá phức tạp sẽ cần phải được viết thì nó sẽ giảm bớt gánh nặng trong quá trình thử nghiệm và có thể thúc đẩy một thiết kế tốt hơn.

1
jpierson

Có, có thể có nhiều xác nhận miễn là một bài kiểm tra thất bại cung cấp cho bạn đủ thông tin để có thể chẩn đoán lỗi. Điều này sẽ phụ thuộc vào những gì bạn đang thử nghiệm và các chế độ thất bại là gì.

Các bài kiểm tra đơn vị thích hợp sẽ thất bại vì chính xác một lý do, đó là lý do tại sao bạn nên sử dụng một xác nhận cho mỗi bài kiểm tra đơn vị.

Tôi chưa bao giờ thấy các công thức như vậy là hữu ích (rằng một lớp nên có một lý do để thay đổi là một ví dụ về một câu ngạn ngữ không ích lợi như vậy). Hãy xem xét một khẳng định rằng hai chuỗi bằng nhau, điều này tương đương về mặt ngữ nghĩa với việc khẳng định rằng độ dài của hai chuỗi là như nhau và mỗi ký tự ở chỉ số tương ứng là bằng nhau.

Chúng ta có thể khái quát và nói rằng bất kỳ hệ thống xác nhận nào cũng có thể được viết lại thành một xác nhận duy nhất và bất kỳ xác nhận đơn lẻ nào cũng có thể được phân tách thành một tập hợp các xác nhận nhỏ hơn.

Vì vậy, chỉ cần tập trung vào sự rõ ràng của mã và sự rõ ràng của kết quả kiểm tra và để điều đó hướng dẫn số lượng xác nhận bạn sử dụng thay vì ngược lại.

1
CurtainDog

Câu hỏi này liên quan đến vấn đề kinh điển về sự cân bằng giữa các vấn đề mã spaghetti và lasagna.

Có nhiều khẳng định có thể dễ dàng gặp phải vấn đề spaghetti khi bạn không biết thử nghiệm đó là gì, nhưng việc xác nhận duy nhất cho mỗi thử nghiệm có thể khiến thử nghiệm của bạn không thể đọc được bằng một thử nghiệm lớn trong một lasagna lớn khiến việc tìm kiếm thử nghiệm không thể thực hiện được .

Có một số trường hợp ngoại lệ, nhưng trong trường hợp này giữ con lắc ở giữa là câu trả lời.

0
gsf

Câu trả lời rất đơn giản - nếu bạn kiểm tra một hàm thay đổi nhiều hơn một thuộc tính, của cùng một đối tượng hoặc thậm chí hai đối tượng khác nhau và tính chính xác của hàm phụ thuộc vào kết quả của tất cả các thay đổi đó, thì bạn muốn khẳng định rằng mỗi một trong những thay đổi đó đã được thực hiện đúng!

Tôi có ý tưởng về một khái niệm logic, nhưng kết luận ngược lại sẽ nói rằng không có chức năng nào phải thay đổi nhiều hơn một đối tượng. Nhưng đó là điều không thể thực hiện trong mọi trường hợp, theo kinh nghiệm của tôi.

Lấy khái niệm logic về giao dịch ngân hàng - rút một số tiền từ một tài khoản ngân hàng trong hầu hết các trường hợp ĐÃ bao gồm việc thêm số tiền đó vào tài khoản khác. Bạn KHÔNG BAO GIỜ muốn tách hai thứ đó ra, chúng tạo thành một đơn vị nguyên tử. Bạn có thể muốn thực hiện hai chức năng (rút/addMoney) và do đó viết hai bài kiểm tra đơn vị khác nhau - ngoài ra. Nhưng hai hành động đó phải diễn ra trong một giao dịch và bạn cũng muốn đảm bảo rằng giao dịch đó hoạt động. Trong trường hợp đó, đơn giản là không đủ để đảm bảo các bước riêng lẻ đã thành công. Bạn phải kiểm tra cả hai tài khoản ngân hàng, trong bài kiểm tra của bạn.

Có thể có nhiều ví dụ phức tạp hơn mà bạn sẽ không kiểm tra trong bài kiểm tra đơn vị, ngay từ đầu, nhưng thay vào đó là bài kiểm tra tích hợp hoặc chấp nhận. Nhưng những ranh giới đó là trôi chảy, IMHO! Không dễ để quyết định, đó là vấn đề hoàn cảnh và có thể là sở thích cá nhân. Rút tiền từ một và thêm nó vào một tài khoản khác vẫn là một chức năng rất đơn giản và chắc chắn là một ứng cử viên để thử nghiệm đơn vị.

0
cslotty