Tôi bối rối về cách làm việc chính xác với TDD


8

Tôi đang cố gắng nắm bắt ý tưởng đằng sau TDD là gì và làm thế nào một nhóm được cho là làm việc với nó. Tôi có trường hợp thử nghiệm sau với NUnit + Moq (chỉ viết bằng bộ nhớ, không đảm bảo các biên dịch ví dụ nhưng cần được giải thích):

[Test]
public void WhenUserLogsCorrectlyIsRedirectedToLoginCorrectView() {
    Mock<IUserDatabaseRepository> repoMock = new Mock<IUserDatabaseRepository>();
    repoMock.Setup(m => m.GetUser(It.IsAny())).Returns(new User { Name = "Peter" });        

    Mock<ILoginHelper> loginHelperMock = new Mock<ILoginHelper>();
    loginHelperMock.Setup(m => m.Login(It.IsAny(), It.IsAny())).Returns(true);
    Mock<IViewModelFactory> factoryMock = new Mock<IViewModelFactory>();
    factoryMock.Setup(m => m.CreateViewModel()).Returns(new LoginViewModel());

    AccountController controller = new AccountController(repoMock.Object, loginHelperMock.Object, factoryMock.Object)

    var result = controller.Index(username : "Peter", password: "whatever");

    Assert.AreEqual(result.Model.Username, "Peter");
}

AccountContoder có 3 phụ thuộc mà tôi giả định rằng, khi được bố trí bên trong bộ điều khiển, cho phép tôi xác minh xem đăng nhập có đúng hay không.

Điều khiến tôi suy nghĩ là ... nếu trong lý thuyết TDD, trước tiên bạn phải viết bộ kiểm thử của bạn và xây dựng mã của bạn từ đó, làm thế nào tôi phải biết trước rằng để thực hiện thao tác của mình, tôi cần sử dụng ba phụ thuộc đó và hoạt động đó sẽ gọi các hoạt động nhất định? Giống như tôi cần biết các phần bên trong của Bài kiểm tra trước khi thực hiện nó để Mock các phụ thuộc và cô lập lớp, tạo ra một số loại kiểm tra viết - viết mã - sửa đổi kiểm tra nếu cần chu kỳ.

Đương nhiên, không có bất kỳ kiến ​​thức nào về bộ mã của tôi và chỉ thể hiện bài kiểm tra, tôi có thể diễn đạt nó giống như nó chỉ cần ILoginHelper và "giả sử" một cách kỳ diệu trước khi viết mã rằng nó sẽ trả lại cho người dùng khi đăng nhập thành công (và cuối cùng nhận ra rằng khung cơ bản không hoạt động theo cách đó, ví dụ, chỉ trả về một ID thay vì đối tượng đầy đủ).

Tôi có hiểu TDD một cách không chính xác không? Đó là một thực hành TDD điển hình trên một trường hợp phức tạp?

Cảm ơn bạn


1
Bạn không phải tuân theo TDD nghiêm ngặt để nhận được nhiều lợi ích nhất từ ​​các bài kiểm tra đơn vị.
Den

2
@Den: "TDD nghiêm ngặt" không có nghĩa là OP tin nó có nghĩa gì.
Doc Brown

Tôi khuyên bạn nên xem vimeo.com/album/3143213/video/71816368 (8LU: Khái niệm nâng cao trong TDD). Nó có thể giúp bạn có được đầu óc của bạn xung quanh mọi thứ.
Andrew Eddie

Câu trả lời:


19

nếu trong lý thuyết TDD, trước tiên bạn phải viết bộ thử nghiệm của mình và xây dựng mã của bạn từ đó

Đây là sự hiểu lầm của bạn. TDD không phải là về việc viết một bộ thử nghiệm đầy đủ trước tiên - đó là một huyền thoại sai lầm. TDD có nghĩa là làm việc theo chu kỳ nhỏ,

  • viết một bài kiểm tra tại một thời điểm
  • chỉ thực hiện càng nhiều mã càng cần thiết để làm cho thử nghiệm "xanh"
  • refactor (mã các bài kiểm tra)

Vì vậy, việc tạo ra một bộ kiểm thử không được thực hiện trong một bước và không phải là "trước khi mã được viết", nó được đan xen với việc triển khai mã được đặt cọc.

Áp dụng cho ví dụ của bạn: bạn nên thử bắt đầu với một thử nghiệm đơn giản cho bộ điều khiển mà không có bất kỳ phụ thuộc nào (giống như nguyên mẫu). Sau đó, bạn thực hiện bộ điều khiển, và tái cấu trúc. Sau đó, bạn có thể thêm một thử nghiệm mới, điều này hy vọng bộ điều khiển của bạn sẽ làm được nhiều hơn một chút hoặc bạn tái cấu trúc / mở rộng thử nghiệm hiện tại của mình. Sau đó, bạn sửa đổi bộ điều khiển của mình cho đến khi thử nghiệm mới trở thành "màu xanh lá cây". Bằng cách đó, bạn bắt đầu với một sự kết hợp đơn giản giữa các bài kiểm tra & môn học đang được kiểm tra, và kết thúc với một bài kiểm tra & môn học phức tạp đang được kiểm tra.

Khi đi tuyến đường này, tại một số thời điểm, bạn sẽ tìm ra dữ liệu bổ sung nào bạn cần làm đầu vào cho bộ điều khiển để thực hiện công việc của mình. Điều này thực sự có thể xảy ra tại một thời điểm mà bạn cố gắng thực hiện một phương thức điều khiển, và không phải khi thiết kế thử nghiệm tiếp theo. Đó là điểm mà bạn dừng để thực hiện phương thức trong một thời gian ngắn và bắt đầu giới thiệu các phụ thuộc bị thiếu trước tiên (có thể bằng cách tái cấu trúc hàm tạo của bộ điều khiển của bạn). Điều này dẫn thẳng đến việc tái cấu trúc các thử nghiệm hiện tại của bạn - trong TDD, trước tiên bạn sẽ thay đổi các thử nghiệm gọi hàm tạo và thêm các thuộc tính hàm tạo mới sau đó. Và đó là nơi mã hóa và viết các bài kiểm tra trở nên hoàn toàn vướng mắc.


13

Điều khiến tôi suy nghĩ là ... nếu trong lý thuyết TDD, trước tiên bạn phải viết bộ thử nghiệm của mình và xây dựng mã của bạn từ đó, làm thế nào tôi phải biết trước rằng để thực hiện thao tác của mình, tôi cần phải sử dụng ba phụ thuộc đó và hoạt động đó sẽ gọi các hoạt động nhất định? Giống như tôi cần biết các phần bên trong của Bài kiểm tra trước khi thực hiện nó để Mock các phụ thuộc và cô lập lớp, tạo ra một số loại kiểm tra viết - viết mã - sửa đổi kiểm tra nếu cần chu kỳ.

Nó cảm thấy sai, phải không? Và nó nên - không phải vì các thử nghiệm của bạn cho bộ điều khiển là không chính xác hoặc "xấu" theo bất kỳ cách nào, mà bởi vì bạn muốn kiểm tra bộ điều khiển trước khi bạn có bất cứ điều gì để được "kiểm soát". :)

Quan điểm của tôi là: TDD sẽ cảm thấy tự nhiên hơn với bạn một khi bạn bắt đầu thực hiện nó ở cấp độ "quy tắc kinh doanh" và "logic ứng dụng thực", đó cũng là nơi hữu ích nhất. Bộ điều khiển thường chỉ giao dịch với ủy quyền cho các thành phần khác, do đó, điều tự nhiên là, để kiểm tra xem việc ủy ​​nhiệm có được thực hiện chính xác hay không, bạn cần biết đối tượng nào sẽ được ủy quyền. Vấn đề duy nhất là khi bạn cố gắng làm điều đó trước khi bạn thực hiện bất kỳ logic thực tế nào. Ví dụ, đề xuất của tôi là bạn cố gắng triển khai lớp Đăng nhập, bằng cách thực hiện TDD theo cách "định hướng hành vi" hơn. Nó sẽ cảm thấy tự nhiên hơn và có thể bạn sẽ thấy nhiều lợi ích hơn.

Vì vậy, với một câu trả lời chung chung hơn: TDD là một cách thực hành mà chúng tôi tạo ra các bài kiểm tra trước khi viết mã chúng tôi cần, nhưng nó không xác định loại thử nghiệm nào. Bộ điều khiển thường là tích hợp của các thành phần, vì vậy bạn viết các bài kiểm tra đơn vị thường đòi hỏi nhiều sự chế giễu. Khi bạn viết logic ứng dụng (quy tắc kinh doanh, chẳng hạn như đặt hàng, xác thực quyền tự động của người dùng, v.v.), bạn viết kiểm tra hành vi, thường sẽ là kiểm tra dựa trên trạng thái (đưa ra đầu vào so với đầu ra mong muốn). Sự khác biệt này thường được cộng đồng TDD gọi là Mockism so với Statism. Tôi là một phần của nhóm (nhỏ) khẳng định rằng cả hai cách đều đúng, chỉ là chúng cung cấp sự đánh đổi khác nhau, do đó hữu ích cho các tình huống khác nhau như được mô tả ở trên.


1
Câu trả lời của bạn có một số điểm tốt, nhưng cho phép tôi viết một điều. "Bộ điều khiển thường là bộ tích hợp của các thành phần, vì vậy bạn viết các bài kiểm tra tích hợp, thường đòi hỏi nhiều sự chế giễu" - tôi đoán bạn có thể có nghĩa là "khi bạn cố gắng viết bài kiểm tra đơn vị cho bộ điều khiển, nó thường sẽ đòi hỏi rất nhiều" . IMHO thuật ngữ "kiểm tra tích hợp" phù hợp hơn cho một thử nghiệm mà không cần chế giễu, trong đó bạn thực sự sử dụng các thành phần thực và không có giả, để xem liệu chúng có hoạt động cùng nhau như dự định không.
Doc Brown

Cảm ơn @DocBrown, tôi thực sự đã đề cập đến một "bài kiểm tra đơn vị kiểm tra sự tích hợp / giao tiếp giữa các thành phần", chứ không phải khái niệm về các bài kiểm tra tích hợp bao gồm các thành phần thực.
MichelHenrich

1
Chà, bây giờ chúng ta đồng ý về thuật ngữ "kiểm tra tích hợp", tôi nghĩ rằng câu trả lời của bạn dẫn thẳng đến câu hỏi tiếp theo: có thực sự đáng để sử dụng TDD (hoặc viết bài kiểm tra đơn vị) cho các bộ điều khiển với vai trò chính là "tích hợp" không? Hoặc người ta chỉ nên viết các bài kiểm tra tích hợp cho các thành phần này (có thể sau đó)?
Doc Brown

4

Mặc dù TDD là phương pháp thử nghiệm đầu tiên, nhưng nó không yêu cầu bạn dành nhiều thời gian để viết mã kiểm tra trước khi bạn viết bất kỳ mã sản xuất nào.

Trong ví dụ này, ý tưởng về TDD được mô tả trong cuốn sách bán kết của Kent Beck về TDD ( 1 ) là bắt đầu với một cái gì đó thực sự đơn giản, như có thể

AccountController controller = new AccountController()

var result = controller.Index(username : "Peter", password: "whatever");

Assert.AreEqual(result.Model.Username, "Peter");

Lúc đầu, bạn không biết mọi thứ bạn sẽ cần bạn thực hiện công việc. Bạn chỉ cần biết rằng bạn sẽ cần một bộ điều khiển với phương thức Index cung cấp cho bạn một mô hình có Tên người dùng. Bạn không biết làm thế nào nó sẽ làm điều đó. Bạn vừa đặt mục tiêu cho chính mình.

Sau đó, bạn có được điều đó để làm việc bằng cách sử dụng bất kỳ phương tiện có sẵn, có thể chỉ là mã hóa kết quả chính xác lúc đầu. Sau đó, trong các lần tái cấu trúc tiếp theo (và thậm chí thêm các bài kiểm tra bổ sung), bạn thêm độ tinh vi cao hơn từng bước một. TDD cho phép bạn thực hiện một bước nhỏ như bạn cần để tiến về phía trước nhưng cũng để bạn tự do thực hiện một bước lớn như kỹ năng và kiến ​​thức của bạn cho phép. Bằng cách thực hiện một chu kỳ ngắn giữa mã kiểm tra và mã sản xuất, bạn sẽ nhận được phản hồi về từng bước nhỏ bạn thực hiện và biết gần như ngay lập tức liệu những gì bạn vừa làm và liệu nó có làm hỏng bất kỳ hoạt động nào trước đó không.

Robert Martin trong ( 2 ) cũng ủng hộ trong một thời gian chu kỳ rất ngắn giữa việc viết mã kiểm tra và viết mã sản xuất.


3

Cuối cùng bạn có thể cần tất cả sự phức tạp này cho một bài kiểm tra đơn vị khái niệm đơn giản, nhưng bạn gần như chắc chắn sẽ không viết bài kiểm tra như thế này ngay từ đầu.

Trước hết, thiết lập phức tạp trong sáu dòng đầu tiên của bạn nên được đưa vào mã cố định có thể tái sử dụng. Các nguyên tắc lập trình có thể duy trì áp dụng cho mã kiểm tra giống như mã doanh nghiệp; nếu bạn sử dụng cùng một vật cố cho hai hoặc nhiều bài kiểm tra, thì nó chắc chắn sẽ được cấu trúc lại thành một phương thức riêng biệt để bạn chỉ có một dòng gây mất tập trung trong bài kiểm tra của bạn hoặc vào mã thiết lập lớp để bạn không có.

Nhưng điều quan trọng hơn: viết bài kiểm tra trước tiên không đảm bảo rằng nó có thể không thay đổi mãi mãi . Nếu bạn không biết các cộng tác viên của một cuộc gọi phương thức, bạn gần như chắc chắn sẽ không thể đoán đúng họ trong lần thử đầu tiên. Không có gì sai khi tái cấu trúc mã kiểm tra của bạn cùng với mã doanh nghiệp của bạn nếu API công khai thay đổi. Đúng là mục đích của TDD là viết API chính xác, có thể sử dụng ngay từ đầu, nhưng điều này hầu như không bao giờ đạt được đến 100%. Yêu cầu luônthay đổi sau thực tế, và tất cả quá thường xuyên, điều này hoàn toàn đòi hỏi các cộng tác viên không tồn tại khi bạn viết đoạn lặp đầu tiên của một câu chuyện. Trong trường hợp đó, không có gì để làm ngoài việc cắn viên đạn và thay đổi các bài kiểm tra hiện có cùng với ứng dụng của bạn; và đó là những dịp mà phần lớn mã thiết lập mà bạn trích dẫn sẽ được đưa vào bộ thử nghiệm của bạn.


2
Tôi không đồng ý mạnh mẽ đến phần đầu tiên. Các xét nghiệm phải độc lập. Đó là một yêu cầu cao hơn nhiều trong các thử nghiệm đơn vị so với mã, vì tính độc lập cải thiện khả năng duy trì của các thử nghiệm đơn vị, trong khi việc thiếu sử dụng lại gây hại cho mã sản xuất.
Telastyn

1
@Telastyn Các thử nghiệm vẫn có thể độc lập trong khi chia sẻ mã thiết lập. Bạn chỉ cần đảm bảo rằng bạn sử dụng một vật cố mới , có nghĩa là gọi một phương thức thiết lập chung hoặc sử dụng thiết lập ngầm (nếu khung kiểm tra của bạn hỗ trợ nó).
Benjamin Hodgson

1
@BenjaminHodgson - Tôi không thấy cách thay đổi phương thức thiết lập chung cho một thử nghiệm và không phá vỡ một thử nghiệm khác.
Telastyn

1
@Telastyn Nhưng điều đó áp dụng cho mã được sử dụng lại nói chung - một khi một lớp có nhiều hơn một khách hàng thì khó thay đổi hơn. Bạn đang tranh luận về việc sao chép + dán sao chép mã thiết lập lịch thi đấu trong tất cả các bài kiểm tra đơn vị?
Benjamin Hodgson

3
@Telastyn: nếu việc kiểm tra độc lập với nhau vi phạm nguyên tắc DRY, bạn chắc chắn sẽ gặp vấn đề khi cố gắng cải thiện thiết kế mã của mình, nhưng phải thay đổi 30 phương thức kiểm tra bằng "thiết lập tương tự" thay vì một phương thức thiết lập được sử dụng lại . Đó thực sự là đối số hàng đầu mà tôi thường nghe chống lại TDD - quá nhiều nỗ lực để thay đổi các bài kiểm tra trong quá trình tái cấu trúc - nhưng hầu như luôn luôn là vấn đề mà các bài kiểm tra không đủ KHÔ.
Doc Brown

2

Giống như tôi cần biết các phần bên trong của Bài kiểm tra trước khi thực hiện nó để Mock các phụ thuộc và cô lập lớp, tạo ra một số loại kiểm tra viết - viết mã - sửa đổi kiểm tra nếu cần chu kỳ.

Vâng, ở một mức độ nào đó bạn làm. Vì vậy, tôi không nghĩ rằng bạn đang hiểu sai về cách TDD hoạt động.

Vấn đề là - như những người khác đã đề cập - ban đầu cảm thấy rất kỳ quặc, gần như sai khi làm theo cách này. Theo ý kiến ​​của tôi, điều đó thực sự thể hiện những gì tôi cảm thấy là lợi ích lớn nhất của TDD: bạn phải hiểu đúng yêu cầu trước khi viết mã.

Là lập trình viên, chúng tôi thích viết mã. Vì vậy, những gì cảm thấy "đúng" và "tự nhiên" đối với chúng tôi là bỏ qua các yêu cầu và bị mắc kẹt càng nhanh càng tốt. Các vấn đề thiết kế sau đó dần dần trở nên rõ ràng khi bạn xây dựng và kiểm tra cơ sở mã. Vì vậy, bạn tái cấu trúc và sửa chữa chúng và mọi thứ dần dần cải thiện và hướng tới mục tiêu của bạn.

Mặc dù vui, đây không phải là cách đặc biệt hiệu quả để làm việc. Tốt hơn hết là nắm bắt chính xác những gì một mô-đun phần mềm nên làm trước tiên, hãy kiểm tra lại và sau đó viết mã. Đó là ít tái cấu trúc, ít bảo trì thử nghiệm hơn và nó buộc bạn vào kiến ​​trúc tốt hơn khỏi khối.

Tôi không làm rất nhiều TDD và tôi nghĩ câu thần chú "bảo hiểm mã 100%" là vô nghĩa. Đặc biệt trong những trường hợp như của bạn. Nhưng việc áp dụng TDD vẫn có nhiều giá trị bởi vì đó là một lợi ích lớn trong việc đảm bảo mọi thứ được thiết kế tốt và duy trì tốt trên mã của bạn.

Vì vậy, tóm lại, thực tế bạn đang tìm thấy điều kỳ quái này có lẽ là một dấu hiệu tốt cho thấy bạn đang đi đúng hướng.


0

Mocking data chỉ là thực hành sử dụng dữ liệu giả .. khung Moq giúp việc tạo dữ liệu giả "dễ dàng hơn".

KIẾM | HÀNH ĐỘNG | XÁC NHẬN

TDD thường là về việc tạo các bài kiểm tra của bạn và sau đó xác nhận các bài kiểm tra đó "vượt qua". Ban đầu, thử nghiệm đầu tiên sẽ thất bại do mã để xác thực thử nghiệm đó chưa được tạo .. Tôi tin rằng đây thực sự là một loại thử nghiệm nhất định; Thử nghiệm "đỏ / xanh", mà tôi chắc chắn là nguồn gốc của các phương pháp "Thử nghiệm theo hướng" ngày nay.

Nói chung, các bài kiểm tra xác nhận các logic nhỏ làm cho mã hình ảnh lớn hơn hoạt động. Bạn có thể bắt đầu ở cấp độ chức năng nhỏ nhất, và sau đó tìm đến các chức năng phức tạp hơn.

vâng, đôi khi việc thiết lập hoặc "chế nhạo" sẽ hơi mãnh liệt, đó là lý do tại sao sử dụng khung moq là một ý tưởng tốt, tuy nhiên, nếu bạn tập trung vào logic kinh doanh cốt lõi, thì các bài kiểm tra của bạn sẽ mang lại kết quả chắc chắn rằng nó hoạt động như mong đợi và dự định.

Cá nhân, tôi không kiểm tra bộ điều khiển của mình vì mọi thứ mà bộ điều khiển đang sử dụng đã được kiểm tra để hoạt động và nói chung, chúng tôi không cần kiểm tra khung.

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.