Tại sao viết bài kiểm tra mã mà tôi sẽ cấu trúc lại?


15

Tôi đang tái cấu trúc một lớp mã kế thừa lớn. Tái cấu trúc (tôi đoán) ủng hộ điều này:

  1. viết bài kiểm tra cho lớp kế thừa
  2. tái cấu trúc quái vật ra khỏi lớp

Vấn đề: một khi tôi cấu trúc lại lớp, các bài kiểm tra của tôi ở bước 1 sẽ cần phải được thay đổi. Ví dụ, những gì đã từng là một phương thức cũ, bây giờ có thể là một lớp riêng thay thế. Một phương thức có thể là gì bây giờ Toàn bộ khung cảnh của lớp kế thừa có thể bị xóa sạch thành một cái gì đó mới, và vì vậy các bài kiểm tra tôi viết ở bước 1 sẽ gần như vô hiệu. Về bản chất, tôi sẽ thêm Bước 3. viết lại các bài kiểm tra của tôi

Mục đích sau đó để viết bài kiểm tra trước khi tái cấu trúc là gì? Nghe có vẻ giống như một bài tập học thuật tạo ra nhiều công việc hơn cho bản thân tôi. Bây giờ tôi đang viết bài kiểm tra cho phương pháp này và tôi đang tìm hiểu thêm về cách kiểm tra mọi thứ và cách thức phương pháp hoạt động. Người ta có thể học được điều này bằng cách chỉ đọc chính mã kế thừa, nhưng viết bài kiểm tra gần giống như dụi mũi vào đó, và cũng ghi lại kiến ​​thức tạm thời này trong các bài kiểm tra riêng biệt. Vì vậy, cách này tôi gần như không có lựa chọn nào khác ngoài việc tìm hiểu mã đang làm gì. Tôi đã nói tạm thời ở đây, bởi vì tôi sẽ cấu trúc lại mã chết và tất cả tài liệu và bài kiểm tra của tôi sẽ không có giá trị đối với một phần quan trọng, ngoại trừ kiến ​​thức của tôi sẽ ở lại và cho phép tôi làm mới hơn về tái cấu trúc.

Đó có phải là lý do thực sự để viết các bài kiểm tra trước khi tái cấu trúc - để giúp tôi hiểu mã tốt hơn? Có một lý do khác!

Vui lòng giải thích!

Ghi chú:

Có bài đăng này: Có ý nghĩa gì khi viết các bài kiểm tra cho mã kế thừa khi không có thời gian để tái cấu trúc hoàn chỉnh? nhưng nó nói "viết bài kiểm tra trước khi tái cấu trúc", nhưng không nói "tại sao" hoặc phải làm gì nếu "bài kiểm tra viết" có vẻ như "công việc bận rộn sẽ bị phá hủy sớm"


1
Tiền đề của bạn là không chính xác. Bạn sẽ không thay đổi bài kiểm tra của mình. Bạn sẽ viết bài kiểm tra mới. Bước 3 sẽ là "xóa bất kỳ bài kiểm tra nào hiện không còn tồn tại."
pdr

1
Bước 3 sau đó có thể đọc "Viết bài kiểm tra mới. Xóa bài kiểm tra không còn tồn tại". Tôi nghĩ rằng nó vẫn còn để phá hủy tác phẩm gốc
Dennis

3
Không, bạn muốn viết các bài kiểm tra mới trong bước 2. Và có, bước 1 bị hủy. Nhưng nó có lãng phí thời gian không? Không, bởi vì nó mang lại cho bạn rất nhiều sự yên tâm rằng bạn sẽ không phá vỡ bất cứ điều gì trong bước 2. Các thử nghiệm mới của bạn không.
pdr

3
@Dennis - mặc dù tôi chia sẻ rất nhiều mối quan tâm của bạn về các tình huống, chúng tôi có thể coi hầu hết nỗ lực tái cấu trúc là "phá hủy công việc ban đầu" nhưng nếu chúng tôi không bao giờ phá hủy nó, chúng tôi sẽ không bao giờ rời khỏi mã spaghetti với 10k dòng trong một tập tin. Tương tự có lẽ nên đi kiểm tra đơn vị, họ đi đôi với mã họ đang kiểm tra. Khi mã phát triển và mọi thứ được di chuyển và / hoặc bị xóa, do đó, các bài kiểm tra đơn vị sẽ phát triển cùng với nó.
DXM

"Hiểu mã" là lợi thế không nhỏ. Làm thế nào để bạn mong muốn cấu trúc lại một chương trình mà bạn không hiểu? Đó là điều không thể tránh khỏi, và cách nào tốt hơn để chứng minh sự hiểu biết thực sự về một chương trình hơn là viết một bài kiểm tra kỹ lưỡng. Ngoài ra, cần phải nói rằng thử nghiệm càng trừu tượng thì càng ít có khả năng bạn sẽ phải cào nó sau này, vì vậy nếu có bất cứ điều gì, hãy thử với thử nghiệm cấp cao lúc đầu.
Neil

Câu trả lời:


46

Tái cấu trúc là làm sạch một đoạn mã (ví dụ: cải thiện kiểu dáng, thiết kế hoặc thuật toán), mà không thay đổi hành vi (hiển thị bên ngoài). Bạn viết các bài kiểm tra không để đảm bảo rằng mã trước và sau khi tái cấu trúc như nhau, thay vào đó bạn viết các bài kiểm tra như một chỉ báo cho thấy ứng dụng của bạn trước và sau khi tái cấu trúc hoạt động giống nhau: Mã mới tương thích và không có lỗi mới nào được đưa ra.

Mối quan tâm chính của bạn là viết các bài kiểm tra đơn vị cho giao diện chung của phần mềm. Giao diện này không nên thay đổi, vì vậy các thử nghiệm (là kiểm tra tự động cho giao diện này) cũng không nên thay đổi.

Tuy nhiên, các bài kiểm tra cũng hữu ích để xác định vị trí lỗi, do đó, có thể có ý nghĩa khi viết bài kiểm tra cho các phần riêng tư của phần mềm của bạn. Những thử nghiệm này dự kiến ​​sẽ thay đổi trong suốt quá trình tái cấu trúc. Nếu bạn muốn thay đổi chi tiết triển khai (như đặt tên hàm riêng), trước tiên bạn cập nhật các kiểm tra để phản ánh mong đợi đã thay đổi của bạn, sau đó đảm bảo rằng thử nghiệm thất bại (kỳ vọng của bạn không được đáp ứng), sau đó bạn thay đổi mã thực tế và kiểm tra xem tất cả các bài kiểm tra vượt qua một lần nữa. Không có lúc nào các bài kiểm tra cho giao diện công cộng bắt đầu thất bại.

Điều này khó khăn hơn khi thực hiện các thay đổi ở quy mô lớn hơn, ví dụ như thiết kế lại nhiều bộ phận phụ thuộc. Nhưng sẽ có một số loại ranh giới, và tại ranh giới đó, bạn sẽ có thể viết bài kiểm tra.


6
+1. Đọc suy nghĩ của tôi, viết câu trả lời của tôi. Điểm quan trọng: bạn có thể cần phải viết các bài kiểm tra đơn vị để chỉ ra rằng các lỗi tương tự vẫn còn đó sau khi tái cấu trúc!
david.pfx

Câu hỏi: tại sao trong ví dụ thay đổi tên hàm của bạn, bạn có thay đổi thử nghiệm trước để đảm bảo nó thất bại không? Tôi muốn nói rằng tất nhiên nó sẽ thất bại khi bạn thay đổi nó - bạn đã phá vỡ kết nối mà các trình liên kết sử dụng để liên kết mã với nhau! Có lẽ bạn đang mong đợi rằng có thể có một chức năng riêng tư hiện có khác theo tên bạn vừa mới chọn và bạn phải xác minh đó không phải là trường hợp trong trường hợp bạn bỏ lỡ nó? Tôi thấy rằng điều này sẽ cung cấp cho bạn sự đảm bảo nhất định giáp với OCD, nhưng trong trường hợp này, nó cảm thấy như một sự quá mức cần thiết. Có bao giờ một lý do có thể kiểm tra trong ví dụ của bạn sẽ không thất bại?
Dennis

^ cont: như một kỹ thuật chung, tôi thấy rằng thật tốt khi thực hiện kiểm tra độ tỉnh táo từng bước của mã để bắt mọi thứ đi sớm nhất có thể. Kiểu như bạn có thể không bị bệnh nếu bạn không rửa tay mỗi lần, nhưng chỉ cần rửa tay như một thói quen sẽ giúp bạn khỏe mạnh hơn, cho dù bạn có tiếp xúc với những thứ bị ô nhiễm hay không. Tại đây, đôi khi bạn có thể rửa tay một cách không cần thiết hoặc kiểm tra mã một cách không cần thiết, nhưng nó giúp bạn và mã của bạn khỏe mạnh. Đó có phải là quan điểm của bạn?
Dennis

@Dennis thực sự, tôi đã vô thức mô tả một thí nghiệm đúng đắn về mặt khoa học: Chúng tôi không thể biết tham số nào thực sự ảnh hưởng đến kết quả khi thay đổi nhiều thông số hơn. Hãy nhớ rằng các bài kiểm tra là mã và mọi mã đều có lỗi. Bạn sẽ đến địa ngục lập trình vì không chạy thử nghiệm trước khi chạm vào mã? Chắc chắn là không: trong khi chạy các bài kiểm tra sẽ là lý tưởng, đó là đánh giá chuyên môn của bạn cho dù điều đó có cần thiết hay không. Lưu ý thêm rằng một bài kiểm tra đã thất bại nếu nó không biên dịch và câu trả lời của tôi cũng có thể áp dụng cho các ngôn ngữ động, không chỉ với các ngôn ngữ tĩnh có trình liên kết.
amon

2
Từ việc sửa các lỗi khác nhau trong khi tái cấu trúc, tôi nhận ra rằng tôi sẽ không thực hiện các động tác mã một cách dễ dàng mà không cần kiểm tra. Các xét nghiệm cảnh báo tôi về "khác biệt" về hành vi / chức năng mà tôi giới thiệu bằng cách thay đổi mã của mình.
Dennis

7

Ah, duy trì hệ thống di sản.

Lý tưởng nhất là các bài kiểm tra của bạn chỉ xử lý lớp thông qua giao diện của nó với phần còn lại của cơ sở mã, các hệ thống khác và / hoặc giao diện người dùng. Giao diện. Bạn không thể cấu trúc lại giao diện mà không ảnh hưởng đến các thành phần ngược dòng hoặc hạ lưu đó. Nếu đó là tất cả một mớ hỗn độn chặt chẽ thì bạn cũng có thể coi nỗ lực viết lại thay vì tái cấu trúc, nhưng chủ yếu là về ngữ nghĩa.

Biên tập: Giả sử một phần mã của bạn đo lường một cái gì đó và nó có chức năng chỉ trả về một giá trị. Giao diện duy nhất đang gọi hàm / phương thức / whatnot và nhận giá trị trả về. Đây là khớp nối lỏng lẻo và dễ dàng để kiểm tra đơn vị. Nếu chương trình chính của bạn có một thành phần phụ quản lý bộ đệm và tất cả các cuộc gọi đến nó phụ thuộc vào chính bộ đệm, một số biến điều khiển và nó sẽ trả lại các thông báo lỗi thông qua một phần khác của mã, thì bạn có thể nói rằng nó được ghép chặt chẽ và nó khó để kiểm tra đơn vị. Bạn vẫn có thể làm điều đó với đủ số lượng đối tượng giả và không có gì, nhưng nó trở nên lộn xộn. Đặc biệt là trong c. Bất kỳ số lượng tái cấu trúc nào cách bộ đệm hoạt động sẽ phá vỡ thành phần phụ.
Kết thúc chỉnh sửa

Nếu bạn đang kiểm tra lớp của mình thông qua các giao diện vẫn ổn định thì các bài kiểm tra của bạn sẽ hợp lệ trước và sau khi tái cấu trúc. Điều này cho phép bạn thực hiện các thay đổi với sự tự tin rằng bạn đã không phá vỡ nó. Ít nhất, tự tin hơn.

Nó cũng cho phép bạn thực hiện các thay đổi gia tăng. Nếu đây là một dự án lớn, tôi không nghĩ rằng bạn sẽ muốn phá bỏ tất cả, xây dựng một hệ thống hoàn toàn mới và sau đó bắt đầu phát triển các thử nghiệm. Bạn có thể thay đổi một phần của nó, kiểm tra nó và đảm bảo rằng sự thay đổi đó không làm giảm phần còn lại của hệ thống. Hoặc nếu có, ít nhất bạn có thể thấy mớ hỗn độn khổng lồ đang phát triển thay vì ngạc nhiên khi nó phát hành.

Mặc dù bạn có thể chia một phương thức thành ba, nhưng chúng vẫn sẽ làm điều tương tự như phương pháp trước đó đã làm, vì vậy bạn có thể thực hiện bài kiểm tra cho phương thức cũ và chia thành ba. Nỗ lực viết bài kiểm tra đầu tiên không bị lãng phí.

Ngoài ra, coi kiến ​​thức của hệ thống kế thừa là "kiến thức tạm thời" sẽ không tốt. Biết làm thế nào trước đây nó đã làm điều đó là rất quan trọng khi nói đến các hệ thống cũ. Rất hữu ích cho câu hỏi cũ của "tại sao nó làm điều đó?"


Tôi nghĩ rằng tôi hiểu, nhưng bạn đã mất tôi trên các giao diện. tức là các bài kiểm tra tôi đang viết bây giờ hãy kiểm tra xem các biến nhất định đã được xác định đúng chưa, sau khi gọi phương thức kiểm tra. Nếu những biến đó được thay đổi hoặc tái cấu trúc, thì đó sẽ là những thử nghiệm của tôi. Lớp kế thừa hiện tại mà tôi đang làm việc không có giao diện / getters / setters mỗi seh, điều này sẽ làm thay đổi thay đổi hoặc ít tốn công hơn. Nhưng một lần nữa tôi không chắc chắn ý của bạn về giao diện khi nói đến mã kế thừa. Có lẽ tôi có thể tạo ra một số? Nhưng đó sẽ là tái cấu trúc.
Dennis

1
Vâng, nếu bạn có một lớp thần làm mọi thứ thì thực sự không có giao diện nào. Nhưng nếu nó gọi một lớp khác, lớp trên cùng đang mong đợi nó hành xử theo một cách nhất định và các bài kiểm tra đơn vị có thể xác minh nó làm như vậy. Tuy nhiên, tôi sẽ không giả vờ rằng bạn sẽ không phải cập nhật kiểm tra đơn vị của mình trong khi tái cấu trúc.
Philip

4

Câu trả lời / nhận thức của riêng tôi:

Từ việc sửa các lỗi khác nhau trong khi tái cấu trúc, tôi nhận ra rằng tôi sẽ không thực hiện các động tác mã một cách dễ dàng mà không cần kiểm tra. Các xét nghiệm cảnh báo tôi về "khác biệt" về hành vi / chức năng mà tôi giới thiệu bằng cách thay đổi mã của mình.

Bạn không cần phải quá cảnh giác khi bạn có bài kiểm tra tốt. Bạn có thể chỉnh sửa mã của bạn trong một thái độ thoải mái hơn. Các xét nghiệm làm xác minh và kiểm tra vệ sinh cho bạn.

Ngoài ra, các bài kiểm tra của tôi vẫn giữ nguyên khá nhiều như tôi đã tái cấu trúc và không bị phá hủy. Tôi thực sự đã nhận thấy một số cơ hội bổ sung để thêm các xác nhận vào các thử nghiệm của mình khi tôi đào sâu hơn vào mã.

CẬP NHẬT

Chà, bây giờ tôi đang thay đổi các thử nghiệm của mình rất nhiều: / Vì tôi đã tái cấu trúc chức năng ban đầu (loại bỏ chức năng và tạo một lớp dọn dẹp mới thay vào đó, di chuyển lông tơ đã từng ở bên trong hàm bên ngoài lớp mới), nên bây giờ kiểm tra theo mã mà tôi đã chạy trước đó có các tham số khác nhau dưới một tên lớp khác và tạo ra các kết quả khác nhau (mã gốc với lông tơ có nhiều kết quả để kiểm tra hơn). Và vì vậy, các bài kiểm tra của tôi cần phản ánh những thay đổi này và về cơ bản tôi đang viết lại các bài kiểm tra của mình thành một điều mới.

Tôi cho rằng có những giải pháp khác tôi có thể làm để tránh viết lại các bài kiểm tra. tức là giữ tên hàm cũ với mã mới và lông tơ bên trong nó ... nhưng tôi không biết liệu đó có phải là ý tưởng tốt nhất hay không và tôi chưa có nhiều kinh nghiệm để đưa ra phán quyết về việc phải làm gì.


Âm thanh giống như bạn thiết kế lại ứng dụng cùng với tái cấu trúc.
JeffO

Khi nào nó tái cấu trúc và khi nào nó được thiết kế lại? tức là khi tái cấu trúc, thật khó để không chia các lớp khó sử dụng thành các lớp nhỏ hơn và cũng di chuyển chúng xung quanh. Vì vậy, có, tôi không chắc chắn chính xác về sự khác biệt, nhưng có lẽ tôi đang làm cả hai.
Dennis

3

Sử dụng các bài kiểm tra của bạn để lái mã của bạn khi bạn làm điều đó. Trong mã kế thừa, điều này có nghĩa là viết các bài kiểm tra cho mã bạn sẽ thay đổi. Bằng cách đó, chúng không phải là một tạo tác riêng biệt. Các thử nghiệm nên là về những gì mã cần phải đạt được chứ không phải về nội tâm của cách thức thực hiện.

Nói chung, bạn muốn thêm các bài kiểm tra về mã không có) cho mã bạn sẽ cấu trúc lại để đảm bảo rằng hành vi mã tiếp tục hoạt động như dự đoán. Do đó, liên tục chạy bộ thử nghiệm trong khi tái cấu trúc là một mạng lưới an toàn tuyệt vời. Ý nghĩ thay đổi mã mà không có bộ kiểm tra để xác nhận những thay đổi không ảnh hưởng đến thứ gì đó không lường trước được là đáng sợ.

Đối với sự nghiêm túc của việc cập nhật các bài kiểm tra cũ, viết bài kiểm tra mới, xóa bài kiểm tra cũ, v.v. Tôi chỉ xem đó là một phần chi phí phát triển phần mềm chuyên nghiệp hiện đại.


Đoạn đầu tiên của bạn dường như ủng hộ việc bỏ qua bước 1 và viết bài kiểm tra khi anh ta đi; đoạn thứ hai của bạn dường như mâu thuẫn với điều đó.
pdr

Cập nhật câu trả lời của tôi.
Michael Durrant

2

Mục tiêu của tái cấu trúc trong trường hợp cụ thể của bạn là gì?

Giả định cho các mục đích đưa ra câu trả lời của tôi mà tất cả chúng ta đều tin (ở một mức độ nào đó) trong TDD (Phát triển dựa trên thử nghiệm).

Nếu mục đích tái cấu trúc của bạn là để dọn sạch mã hiện tại mà không thay đổi hành vi hiện có, thì viết kiểm tra trước khi tái cấu trúc là cách bạn đảm bảo rằng bạn không thay đổi hành vi của mã, nếu bạn thành công, thì các thử nghiệm sẽ thành công cả trước và sau bạn tái cấu trúc.

  • Các bài kiểm tra sẽ giúp bạn đảm bảo rằng công việc mới của bạn thực sự hoạt động.

  • Các thử nghiệm có thể cũng sẽ phát hiện ra các trường hợp mà tác phẩm gốc không hoạt động.

Nhưng làm thế nào để bạn thực sự thực hiện bất kỳ tái cấu trúc đáng kể nào mà không ảnh hưởng đến hành vi ở một mức độ nào đó?

Dưới đây là danh sách ngắn về một vài điều có thể xảy ra trong quá trình tái cấu trúc:

  • đổi tên biến
  • đổi tên hàm
  • thêm chức năng
  • xóa chức năng
  • chia chức năng thành hai hoặc nhiều chức năng
  • kết hợp hai hoặc nhiều chức năng thành một chức năng
  • chia lớp
  • kết hợp các lớp học
  • đổi tên lớp

Tôi sẽ tranh luận rằng mỗi một trong số các hoạt động được liệt kê đó đều thay đổi hành vi theo một cách nào đó.

Và tôi sẽ tranh luận rằng nếu tái cấu trúc của bạn thay đổi hành vi, các bài kiểm tra của bạn vẫn còn sẽ là cách bạn đảm bảo rằng bạn không phá vỡ bất cứ điều gì.

Có thể hành vi không thay đổi ở cấp vĩ mô, nhưng điểm kiểm tra đơn vị không phải là để đảm bảo hành vi vĩ mô. Đó là sự tích hợp thử nghiệm . Điểm của thử nghiệm đơn vị là đảm bảo rằng các bit và phần riêng lẻ mà bạn xây dựng sản phẩm của mình không bị hỏng. Chuỗi, liên kết yếu nhất, vv

Làm thế nào về kịch bản này:

  • Giả sử bạn có function bar()

  • function foo() gọi cho bar()

  • function flee() cũng thực hiện cuộc gọi đến chức năng bar()

  • Chỉ để đa dạng, flam()hãy gọifoo()

  • Tất cả mọi thứ hoạt động lộng lẫy (rõ ràng, ít nhất).

  • Bạn tái cấu trúc ...

  • bar() được đổi tên thành barista()

  • flee() được thay đổi để gọi barista()

  • foo()không thay đổi để cuộc gọibarista()

Rõ ràng, các bài kiểm tra của bạn cho cả hai foo()flam()bây giờ thất bại.

Có lẽ bạn đã không nhận ra foo()được gọi bar()ở nơi đầu tiên. Bạn chắc chắn đã không nhận ra rằng flam()phụ thuộc vào bar()bằng cách foo().

Bất cứ điều gì. Vấn đề là các bài kiểm tra của bạn sẽ phát hiện ra hành vi mới bị phá vỡ của cả hai foo()flam(), theo cách tăng dần trong quá trình tái cấu trúc của bạn.

Các bài kiểm tra kết thúc giúp bạn tái cấu trúc tốt.

Trừ khi bạn không có bất kỳ bài kiểm tra nào.

Đó là một ví dụ giả định. Có những người sẽ lập luận rằng nếu thay đổi bar()nghỉ foo(), thì foo()quá phức tạp để bắt đầu và nên bị phá vỡ. Nhưng các thủ tục có thể gọi các thủ tục khác vì một lý do và không thể loại bỏ tất cả sự phức tạp, phải không? Công việc của chúng tôi là quản lý sự phức tạp hợp lý tốt.

Hãy xem xét một kịch bản khác.

Bạn đang xây dựng một tòa nhà.

Bạn xây dựng một giàn giáo để giúp đảm bảo rằng tòa nhà được xây dựng chính xác.

Giàn giáo giúp bạn xây dựng một trục thang máy, trong số những thứ khác. Sau đó, bạn phá bỏ giàn giáo, nhưng trục thang máy vẫn còn. Bạn đã phá hủy "tác phẩm gốc" bằng cách phá hủy giàn giáo.

Sự tương tự là khó khăn, nhưng vấn đề là nó không phải là chưa từng thấy để xây dựng các công cụ để giúp bạn xây dựng sản phẩm. Ngay cả khi các công cụ không cố định, chúng vẫn hữu ích (thậm chí cần thiết). Thợ mộc làm đồ gá mọi lúc, đôi khi chỉ cho một công việc. Sau đó, họ xé các đồ gá ra, đôi khi sử dụng các bộ phận để xây dựng các đồ gá khác cho các công việc khác, đôi khi không. Nhưng điều đó không làm cho các đồ gá lắp vô dụng hoặc lãng phí công sức.

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.