Làm thế nào để bạn giữ cho các bài kiểm tra đơn vị của bạn làm việc khi tái cấu trúc?


29

Trong một câu hỏi khác, nó đã được tiết lộ rằng một trong những khó khăn với TDD là giữ cho bộ thử nghiệm đồng bộ với codebase trong và sau khi tái cấu trúc.

Bây giờ, tôi là một fan hâm mộ lớn của tái cấu trúc. Tôi sẽ không từ bỏ nó để làm TDD. Nhưng tôi cũng đã trải qua các vấn đề của các bài kiểm tra được viết theo cách mà việc tái cấu trúc nhỏ dẫn đến nhiều thất bại trong bài kiểm tra.

Làm thế nào để bạn tránh phá vỡ các bài kiểm tra khi tái cấu trúc?

  • Bạn có viết bài kiểm tra 'tốt hơn' không? Nếu vậy, bạn nên tìm gì?
  • Bạn có tránh một số loại tái cấu trúc?
  • Có công cụ tái cấu trúc thử nghiệm?

Chỉnh sửa: Tôi đã viết một câu hỏi mới hỏi tôi muốn hỏi gì (nhưng vẫn giữ câu hỏi này như một biến thể thú vị).


7
Tôi đã nghĩ rằng, với TDD, bước đầu tiên của bạn trong tái cấu trúc là viết một bài kiểm tra thất bại và sau đó cấu trúc lại mã để làm cho nó hoạt động.
Matt Ellen

IDE của bạn không thể tìm ra cách cấu trúc lại các bài kiểm tra?

@ Thorbjørn Ravn Andersen, vâng, và tôi đã viết một câu hỏi mới hỏi tôi muốn hỏi gì (nhưng giữ câu hỏi này như một biến thể thú vị; xem câu trả lời của azheglov, về cơ bản là những gì bạn nói)
Alex Feinman

Xem xét thêm thông tin thar cho câu hỏi này?

Câu trả lời:


35

Những gì bạn đang cố gắng không thực sự tái cấu trúc. Với refactoring, theo định nghĩa, bạn không thay đổi phần mềm của bạn có, bạn thay đổi như thế nào nó có phải nó.

Bắt đầu với tất cả các test màu xanh lá cây (tất cả các đường chuyền), sau đó làm thay đổi "under the hood" (ví dụ như di chuyển một phương pháp từ một lớp học có nguồn gốc đến cơ sở, trích xuất một phương pháp, hoặc đóng gói một composite với một Builder , vv). Bài kiểm tra của bạn vẫn nên vượt qua.

Những gì bạn mô tả dường như không phải là tái cấu trúc, mà là một thiết kế lại, cũng tăng cường chức năng của phần mềm của bạn đang được thử nghiệm. TDD và tái cấu trúc (như tôi đã cố gắng xác định nó ở đây) không xung đột. Bạn vẫn có thể tái cấu trúc (xanh lục) và áp dụng TDD (đỏ-xanh) để phát triển chức năng "delta".


7
Cùng mã X sao chép 15 địa điểm. Tùy chỉnh ở mỗi nơi. Bạn biến nó thành một thư viện chung và tham số hóa X hoặc sử dụng mẫu chiến lược để cho phép những khác biệt này. Tôi đảm bảo các bài kiểm tra đơn vị cho X sẽ thất bại. Khách hàng của X sẽ thất bại vì giao diện công cộng thay đổi một chút. Thiết kế lại hay tái cấu trúc? Tôi gọi nó là refactor nhưng một trong hai cách nó phá vỡ mọi thứ. Điểm mấu chốt là bạn không thể tái cấu trúc trừ khi bạn biết chính xác làm thế nào tất cả khớp với nhau. Sau đó sửa chữa các bài kiểm tra là tẻ nhạt nhưng cuối cùng là tầm thường.
Kevin

3
Nếu các bài kiểm tra cần điều chỉnh liên tục, có lẽ đó là một gợi ý về việc kiểm tra quá chi tiết. Ví dụ, giả sử rằng một đoạn mã cần kích hoạt các sự kiện A, B và C trong các trường hợp nhất định, không theo thứ tự cụ thể. Mã cũ thực hiện theo thứ tự ABC và các thử nghiệm mong đợi các sự kiện theo thứ tự đó. Nếu mã được cấu trúc lại tạo ra các sự kiện theo thứ tự ACB, nó vẫn hoạt động theo thông số kỹ thuật nhưng thử nghiệm sẽ thất bại.
otto

3
@Kevin: Tôi tin rằng những gì bạn mô tả là một thiết kế lại, bởi vì giao diện công cộng thay đổi. Định nghĩa tái cấu trúc của Fowler ("thay đổi cấu trúc bên trong của mã mà không thay đổi hành vi bên ngoài của nó") khá rõ ràng về điều đó.
azheglov

3
@azheglov: có thể nhưng theo kinh nghiệm của tôi nếu việc triển khai không tốt thì giao diện cũng vậy
Kevin

2
Một câu hỏi hoàn toàn hợp lệ và rõ ràng kết thúc theo nghĩa của một cuộc thảo luận về từ ngữ. Ai quan tâm bạn gọi nó như thế nào, hãy thảo luận ở một nơi khác. Trong thời gian đó, câu trả lời này hoàn toàn bỏ qua bất kỳ câu trả lời thực sự nào nhưng vẫn có nhiều sự ủng hộ nhất cho đến nay. Tôi hiểu tại sao mọi người coi TDD là một tôn giáo.
Dirk Boer

21

Một trong những lợi ích của việc kiểm tra đơn vị là để bạn có thể tự tin tái cấu trúc.

Nếu tái cấu trúc không thay đổi giao diện chung thì bạn để nguyên các bài kiểm tra đơn vị và đảm bảo sau khi tái cấu trúc tất cả chúng đều vượt qua.

Nếu tái cấu trúc không thay đổi giao diện công cộng thì các bài kiểm tra nên được viết lại trước tiên. Tái cấu trúc cho đến khi các bài kiểm tra mới vượt qua.

Tôi sẽ không bao giờ tránh bất kỳ tái cấu trúc nào vì nó phá vỡ các bài kiểm tra. Viết bài kiểm tra đơn vị có thể là một nỗi đau ở mông nhưng nó đáng giá về lâu dài.


7

Trái với các câu trả lời khác, điều quan trọng cần lưu ý là một số cách kiểm tra có thể trở nên mong manh khi hệ thống được kiểm tra (SUT) được tái cấu trúc, nếu thử nghiệm là whitebox.

Nếu tôi đang sử dụng khung mô phỏng để xác minh thứ tự của các phương thức được gọi trên các giả (khi thứ tự không liên quan vì các cuộc gọi không có tác dụng phụ); sau đó nếu mã của tôi sạch hơn với các lệnh gọi phương thức đó theo một thứ tự khác và tôi cấu trúc lại, thì thử nghiệm của tôi sẽ bị hỏng. Nói chung, giả có thể giới thiệu sự mong manh cho các bài kiểm tra.

Nếu tôi đang kiểm tra trạng thái bên trong SUT của mình bằng cách hiển thị các thành viên riêng tư hoặc được bảo vệ của nó (chúng ta có thể sử dụng "bạn bè" trong cơ bản trực quan hoặc tăng mức độ truy cập "nội bộ" và sử dụng "internalsvisibleto" trong c #; trong nhiều ngôn ngữ OO, bao gồm cả c # a " phân lớp cụ thể kiểm tra " có thể được sử dụng) sau đó đột nhiên trạng thái bên trong của lớp sẽ có vấn đề - bạn có thể tái cấu trúc lớp dưới dạng hộp đen, nhưng kiểm tra hộp trắng sẽ thất bại. Giả sử một trường duy nhất được sử dụng lại có nghĩa là những thứ khác nhau (không phải là thông lệ tốt!) Khi SUT thay đổi trạng thái - nếu chúng ta chia nó thành hai trường, chúng ta có thể cần phải viết lại các bài kiểm tra bị hỏng.

Các lớp con đặc thù kiểm thử cũng có thể được sử dụng để kiểm tra các phương thức được bảo vệ - điều này có thể có nghĩa là một bộ tái cấu trúc theo quan điểm của mã sản xuất là một sự thay đổi đột phá từ quan điểm của mã kiểm thử. Di chuyển một vài dòng vào hoặc ra khỏi một phương thức được bảo vệ có thể không có tác dụng phụ sản xuất, nhưng phá vỡ một thử nghiệm.

Nếu tôi sử dụng " móc thử nghiệm " hoặc bất kỳ mã biên dịch cụ thể hoặc kiểm tra cụ thể nào khác, có thể khó đảm bảo rằng các thử nghiệm không bị phá vỡ do phụ thuộc mong manh vào logic bên trong.

Vì vậy, để ngăn các bài kiểm tra trở nên kết hợp với các chi tiết bên trong thân mật của SUT, có thể giúp:

  • Sử dụng sơ khai thay vì giả, nếu có thể. Để biết thêm thông tin, hãy xem blog của Fabio Periera về các bài kiểm tra tautologicalblog của tôi về các bài kiểm tra tautological .
  • Nếu sử dụng giả, tránh xác minh thứ tự của các phương thức được gọi, trừ khi nó quan trọng.
  • Cố gắng tránh xác minh trạng thái bên trong của SUT của bạn - sử dụng API bên ngoài của nó nếu có thể.
  • Cố gắng tránh logic cụ thể kiểm tra trong mã sản xuất
  • Cố gắng tránh sử dụng các lớp con kiểm tra cụ thể.

Tất cả các điểm trên là ví dụ về khớp nối hộp trắng được sử dụng trong các thử nghiệm. Vì vậy, để tránh hoàn toàn việc tái cấu trúc các thử nghiệm phá vỡ, hãy sử dụng thử nghiệm hộp đen của SUT.

Tuyên bố miễn trừ trách nhiệm: Với mục đích thảo luận về tái cấu trúc ở đây, tôi đang sử dụng từ rộng hơn một chút để bao gồm thay đổi triển khai nội bộ mà không có bất kỳ tác động bên ngoài rõ ràng nào. Một số người theo chủ nghĩa thuần túy có thể không đồng ý và chỉ đề cập đến cuốn sách Tái cấu trúc của Martin Fowler và Kent Beck - mô tả các hoạt động tái cấu trúc nguyên tử.

Trong thực tế, chúng ta có xu hướng thực hiện các bước không phá vỡ lớn hơn một chút so với các hoạt động nguyên tử được mô tả ở đó và đặc biệt những thay đổi khiến mã sản xuất hoạt động giống hệt từ bên ngoài có thể không để các thử nghiệm đi qua. Nhưng tôi nghĩ thật công bằng khi bao gồm "thuật toán thay thế cho một thuật toán khác có hành vi giống hệt nhau" như một công cụ tái cấu trúc và tôi nghĩ Fowler đồng ý. Chính Martin Fowler nói rằng tái cấu trúc có thể phá vỡ các bài kiểm tra:

Khi bạn viết một bài kiểm tra giả, bạn đang kiểm tra các cuộc gọi đi của SUT để đảm bảo nó nói đúng với các nhà cung cấp của nó. Một thử nghiệm cổ điển chỉ quan tâm đến trạng thái cuối cùng - không phải là trạng thái đó được bắt nguồn như thế nào. Do đó, các thử nghiệm giả lập được kết hợp chặt chẽ hơn với việc thực hiện một phương pháp. Thay đổi bản chất của các cuộc gọi đến cộng tác viên thường khiến một bài kiểm tra giả bị phá vỡ.

[...]

Khớp nối với việc thực hiện cũng can thiệp vào tái cấu trúc, vì các thay đổi triển khai có nhiều khả năng phá vỡ các thử nghiệm hơn so với thử nghiệm cổ điển.

Fowler - Mocks không cuống


Fowler thực sự đã viết cuốn sách về Tái cấu trúc; và cuốn sách có thẩm quyền nhất về thử nghiệm Đơn vị (xUnit Test Forms của Gerard Meszaros) nằm trong sê-ri "chữ ký" của Fowler, vì vậy khi ông nói rằng phép tái cấu trúc có thể phá vỡ một thử nghiệm, có lẽ ông đã đúng.
cầu toàn

5

Nếu các bài kiểm tra của bạn bị hỏng khi bạn tái cấu trúc, thì theo định nghĩa, tái cấu trúc, đó là "thay đổi cấu trúc chương trình của bạn mà không thay đổi hành vi của chương trình của bạn".

Đôi khi bạn cần phải thay đổi hành vi của các bài kiểm tra của bạn. Có thể bạn cần hợp nhất hai phương thức với nhau (giả sử, liên kết () và lắng nghe () trên lớp ổ cắm TCP nghe), do đó bạn có các phần khác trong mã của mình đang cố gắng và không sử dụng API đã bị thay đổi. Nhưng đó không phải là tái cấu trúc!


Điều gì sẽ xảy ra nếu anh ta chỉ thay đổi tên của một phương thức được kiểm tra bằng các thử nghiệm? Các bài kiểm tra sẽ thất bại trừ khi bạn đổi tên chúng trong các bài kiểm tra. Ở đây anh ta không thay đổi hành vi của chương trình.
Oscar Mederos

2
Trong trường hợp đó, các bài kiểm tra của anh ta cũng đang được tái cấu trúc. Bạn cần phải cẩn thận: đầu tiên bạn đổi tên phương thức, sau đó bạn chạy thử nghiệm. Nó sẽ thất bại vì những lý do chính đáng (nó không thể biên dịch (C #), bạn nhận được một ngoại lệ MessageNotUnder Hiểu (Smalltalk), dường như không có gì xảy ra (mô hình ăn không có mục tiêu của Objective-C)). Sau đó, bạn thay đổi bài kiểm tra của mình, biết rằng bạn đã vô tình đưa ra bất kỳ lỗi nào. "Nếu các bài kiểm tra của bạn bị hỏng" có nghĩa là "nếu các bài kiểm tra của bạn bị hỏng sau khi bạn hoàn thành việc tái cấu trúc", nói cách khác. Hãy cố gắng giữ cho khối thay đổi nhỏ!
Frank Shearar

1
Các bài kiểm tra đơn vị vốn đã được ghép nối với cấu trúc của mã. Ví dụ: Fowler có nhiều trong refactoring.com/catalog sẽ ảnh hưởng đến các thử nghiệm đơn vị (ví dụ: phương thức ẩn, phương thức nội tuyến, thay thế mã lỗi bằng ngoại lệ, v.v.).
Kristian H

sai. Hợp nhất hai phương thức với nhau rõ ràng là một phép tái cấu trúc có tên chính thức (ví dụ: tái cấu trúc phương thức nội tuyến phù hợp với định nghĩa) và nó sẽ phá vỡ các thử nghiệm của một phương thức được gạch chân - một số trường hợp kiểm thử bây giờ nên được viết lại / kiểm tra bằng các phương tiện khác. Tôi không phải thay đổi hành vi của một chương trình để phá vỡ các bài kiểm tra đơn vị, tất cả những gì tôi cần làm là cơ cấu lại các phần bên trong có các bài kiểm tra đơn vị kết hợp với chúng. Miễn là hành vi của một chương trình không thay đổi, điều này vẫn phù hợp với định nghĩa tái cấu trúc.
KolA

Tôi đã viết các giả định ở trên với các thử nghiệm được viết tốt: nếu bạn đang thử nghiệm triển khai của mình - nếu cấu trúc của thử nghiệm phản ánh các phần bên trong của mã được thử nghiệm, chắc chắn. Trong trường hợp nào, kiểm tra hợp đồng của đơn vị, không thực hiện.
Frank Shearar

4

Tôi nghĩ rằng rắc rối với câu hỏi này, là những người khác nhau đang dùng từ 'tái cấu trúc' khác nhau. Tôi nghĩ rằng tốt nhất là xác định cẩn thận một vài điều bạn có thể có nghĩa là:

>  Keep the API the same, but change how the API is implemented internally
>  Change the API

Như một người khác đã lưu ý, nếu bạn giữ API như cũ và tất cả các bài kiểm tra hồi quy của bạn hoạt động trên API công khai, bạn sẽ không gặp vấn đề gì. Tái cấu trúc nên không gây ra vấn đề gì cả. Bất kỳ thử nghiệm thất bại nào EITHER có nghĩa là mã cũ của bạn có lỗi và thử nghiệm của bạn không tốt hoặc mã mới của bạn có lỗi.

Nhưng đó là khá rõ ràng. Vì vậy, bạn có nghĩa là bằng cách tái cấu trúc, rằng bạn đang thay đổi API.

Vì vậy, hãy để tôi trả lời làm thế nào để tiếp cận điều đó!

  • Trước tiên hãy tạo API MỚI, đó là những gì bạn muốn hành vi API MỚI của mình. Nếu điều đó xảy ra là API mới này có cùng tên với API OLTER, thì tôi sẽ thêm tên _NEW vào tên API mới.

    int DoS SomethingInterestingAPI ();

trở thành:

int DoSomethingInterestingAPI_NEW( int takes_more_arguments );
int DoSomethingInterestingAPI_OLD();
int DoSomethingInterestingAPI() { DoSomethingInterestingAPI_NEW (whatever_default_mimics_the_old_API);

OK - ở giai đoạn này - tất cả các bài kiểm tra hồi quy của bạn đều vượt qua - sử dụng tên DoS SomethingInterestingAPI ().

TIẾP THEO, xem qua mã của bạn và thay đổi tất cả các cuộc gọi thành DoS SomethingInterestingAPI () thành biến thể thích hợp của DoS SomethingInterestingAPI_NEW (). Điều này bao gồm cập nhật / viết lại bất kỳ phần nào trong các bài kiểm tra hồi quy của bạn cần được thay đổi để sử dụng API mới.

TIẾP THEO, đánh dấu DoS SomethingInterestingAPI_OLD () là [[deprecated ()]]. Giữ xung quanh API không dùng nữa miễn là bạn muốn (cho đến khi bạn cập nhật an toàn tất cả các mã có thể phụ thuộc vào nó).

Với phương pháp này, bất kỳ thất bại nào trong các bài kiểm tra hồi quy của bạn chỉ đơn giản là các lỗi trong bài kiểm tra hồi quy đó hoặc xác định các lỗi trong mã của bạn - chính xác như bạn muốn. Quá trình dàn dựng này để sửa đổi API bằng cách tạo rõ ràng các phiên bản _NEW và _OLD của API cho phép bạn có các bit của mã mới và mã cũ cùng tồn tại trong một thời gian.


Tôi thích câu trả lời này vì nó rõ ràng rằng Bài kiểm tra đơn vị cho SUT giống như các ứng dụng khách bên ngoài đối với Api đã xuất bản. Những gì bạn kê đơn rất giống với giao thức SemVer để quản lý thư viện / thành phần được xuất bản để tránh 'địa ngục phụ thuộc'. Tuy nhiên, điều này đi kèm với chi phí thời gian và tính linh hoạt, ngoại suy cách tiếp cận này với giao diện công cộng của mọi đơn vị vi mô đồng nghĩa với chi phí ngoại suy. Một cách tiếp cận linh hoạt hơn là tách các thử nghiệm khỏi việc thực hiện càng nhiều càng tốt, tức là thử nghiệm tích hợp hoặc DSL riêng để mô tả các đầu vào và đầu ra thử nghiệm
KolA

1

Tôi giả sử các bài kiểm tra đơn vị của bạn có độ chi tiết mà tôi sẽ gọi là "ngu ngốc" :) tức là, họ kiểm tra các chi tiết nhỏ tuyệt đối của mỗi lớp và chức năng. Bước ra khỏi các công cụ tạo mã và viết các bài kiểm tra áp dụng cho bề mặt lớn hơn, sau đó bạn có thể cấu trúc lại các phần bên trong bao nhiêu tùy ý, biết rằng các giao diện cho các ứng dụng của bạn không thay đổi và các bài kiểm tra của bạn vẫn hoạt động.

Nếu bạn muốn có các bài kiểm tra đơn vị kiểm tra từng phương thức, thì bạn sẽ phải cấu trúc lại chúng cùng một lúc.


1
Câu trả lời hữu ích nhất thực sự giải quyết câu hỏi - không xây dựng phạm vi kiểm tra của bạn trên nền tảng run rẩy của những câu đố bên trong, hoặc hy vọng nó sẽ liên tục sụp đổ - nhưng hầu hết đều bị đánh giá thấp bởi vì TDD quy định hoàn toàn ngược lại. Đây là những gì bạn nhận được khi chỉ ra sự thật bất tiện về cách tiếp cận quá cường điệu.
KolA

1

giữ cho bộ kiểm thử đồng bộ hóa với cơ sở mã trong và sau khi tái cấu trúc

Điều gây khó khăn là khớp nối . Bất kỳ thử nghiệm nào đi kèm với một số mức độ khớp nối với các chi tiết triển khai nhưng các thử nghiệm đơn vị (bất kể đó là TDD hay không) đều đặc biệt xấu vì chúng can thiệp vào bên trong: nhiều thử nghiệm đơn vị tương đương với nhiều mã hơn được ghép với các đơn vị nghĩa là chữ ký phương thức / bất kỳ giao diện công khai nào khác của các đơn vị - ít nhất là

"Đơn vị" theo định nghĩa là chi tiết triển khai ở mức độ thấp, giao diện của các đơn vị có thể và nên thay đổi / tách / hợp nhất và nếu không thì sẽ thay đổi khi hệ thống phát triển. Sự phong phú của các bài kiểm tra đơn vị thực sự có thể cản trở sự tiến hóa này nhiều hơn là nó giúp.

Làm thế nào để tránh phá vỡ các bài kiểm tra khi tái cấu trúc? Tránh khớp nối. Trong thực tế, điều đó có nghĩa là tránh các thử nghiệm đơn vị càng nhiều càng tốt và thích các thử nghiệm tích hợp / mức độ cao hơn không rõ ràng hơn về các chi tiết thực hiện. Hãy nhớ rằng mặc dù không có viên đạn bạc, các bài kiểm tra vẫn phải kết hợp với một mức độ nào đó nhưng lý tưởng nhất là giao diện được phiên bản rõ ràng bằng cách sử dụng Phiên bản ngữ nghĩa, thường là ở cấp độ ứng dụng / ứng dụng đã xuất bản (bạn không muốn thực hiện SemVer cho mỗi đơn vị trong giải pháp của bạn).


0

Các bài kiểm tra của bạn quá chặt chẽ với việc thực hiện và không phải là yêu cầu.

xem xét viết bài kiểm tra của bạn với ý kiến ​​như thế này:

//given something
...test code...
//and something else
...test code...
//when something happens
...test code...
//then the state should be...
...test code...

bằng cách này, bạn không thể cấu trúc lại ý nghĩa của các bài kiểm tra.

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.