Có phải là một thực hành xấu để sửa đổi mã nghiêm ngặt cho mục đích thử nghiệm


77

Tôi có một cuộc tranh luận với một đồng nghiệp lập trình viên về việc đó là một cách thực hành tốt hay xấu để sửa đổi một đoạn mã làm việc chỉ để làm cho nó có thể kiểm tra được (ví dụ như thông qua các bài kiểm tra đơn vị).

Ý kiến ​​của tôi là nó ổn, trong giới hạn của việc duy trì các thực hành kỹ thuật phần mềm và hướng đối tượng tốt tất nhiên (không "làm mọi thứ công khai", v.v.).

Ý kiến ​​của đồng nghiệp của tôi là sửa đổi mã (chỉ hoạt động) cho mục đích thử nghiệm là sai.

Chỉ là một ví dụ đơn giản, hãy nghĩ về đoạn mã này được sử dụng bởi một số thành phần (được viết bằng C #):

public void DoSomethingOnAllTypes()
{
    var types = Assembly.GetExecutingAssembly().GetTypes();

    foreach (var currentType in types)
    {
        // do something with this type (e.g: read it's attributes, process, etc).
    }
}

Tôi đã gợi ý rằng mã này có thể được sửa đổi để gọi ra một phương thức khác sẽ thực hiện công việc thực tế:

public void DoSomething(Assembly asm)
{
    // not relying on Assembly.GetExecutingAssembly() anymore...
}

Phương pháp này đưa vào một đối tượng hội để làm việc, làm cho nó có thể vượt qua hội của riêng bạn để thực hiện thử nghiệm trên. Đồng nghiệp của tôi đã không nghĩ rằng đây là một thực hành tốt.

Điều gì được coi là một thực hành tốt và phổ biến?


5
Phương pháp sửa đổi của bạn nên lấy Typemột tham số, không phải là một Assembly.
Robert Harvey

Bạn đúng - Nhập hoặc hội, điểm quan trọng là mã sẽ cho phép cung cấp dưới dạng tham số, mặc dù có vẻ như chức năng này chỉ được sử dụng để thử nghiệm ...
liortal

5
Xe của bạn có cổng ODBI để "thử nghiệm" - nếu mọi thứ hoàn hảo thì sẽ không cần thiết. Tôi đoán là bạn xe đáng tin cậy hơn phần mềm của mình.
mattnz

24
Anh ta có đảm bảo gì về việc mã "hoạt động"?
Craige

3
Tất nhiên - làm như vậy. Mã thử nghiệm ổn định theo thời gian. Nhưng bạn sẽ có thời gian dễ dàng hơn để giải thích các lỗi mới được giới thiệu nếu chúng đi kèm với một số cải tiến về tính năng thay vì sửa lỗi để kiểm tra
Petter Nordlander

Câu trả lời:


135

Sửa đổi mã để làm cho nó dễ kiểm tra hơn có lợi ích vượt quá khả năng kiểm tra. Nói chung, mã dễ kiểm tra hơn

  • Dễ bảo trì hơn,
  • Là dễ dàng hơn để lý do về,
  • Được kết nối lỏng lẻo hơn, và
  • Có thiết kế tổng thể tốt hơn, về mặt kiến ​​trúc.

3
Tôi đã không đề cập đến những điều này trong câu trả lời của mình, nhưng tôi hoàn toàn đồng ý. Làm lại mã của bạn để dễ kiểm tra hơn có xu hướng có một số tác dụng phụ hạnh phúc.
Jason Swett

2
+1: Tôi thường đồng ý. Chắc chắn có những trường hợp rõ ràng trong đó việc làm cho mã có thể kiểm tra được gây hại cho các mục tiêu có lợi khác này và trong những trường hợp đó, khả năng kiểm tra nằm ở mức ưu tiên thấp nhất. Nhưng nói chung, nếu mã của bạn không đủ linh hoạt / có thể mở rộng để kiểm tra, thì mã đó sẽ không đủ linh hoạt / có thể mở rộng để sử dụng hoặc để tuổi một cách duyên dáng.
Telastyn

6
Mã kiểm tra là mã tốt hơn. Luôn luôn có rủi ro cố hữu trong việc sửa đổi mã mà không có các thử nghiệm xung quanh nó, và cách dễ nhất, rẻ nhất để giảm thiểu rủi ro đó trong thời gian cực ngắn là để nó yên, điều đó tốt ... cho đến khi bạn thực sự cần để thay đổi mã. Những gì bạn cần làm là bán lợi ích của thử nghiệm đơn vị cho đồng nghiệp của bạn; nếu tất cả mọi người tham gia thử nghiệm đơn vị, thì không thể có tranh luận rằng mã cần phải được kiểm tra. Nếu không phải tất cả mọi người trên tàu với thử nghiệm đơn vị thì không có điểm nào.
chàng trai

3
Chính xác. Tôi nhớ rằng tôi đã viết bài kiểm tra đơn vị cho khoảng 10k dòng mã của riêng tôi. Tôi biết mã đã hoạt động hoàn hảo nhưng các bài kiểm tra buộc tôi phải suy nghĩ lại về một số tình huống. Tôi đã phải tự hỏi mình "Chính xác thì phương pháp này đang làm gì?". Tôi đã tìm thấy một số lỗi trong mã làm việc chỉ bằng cách nhìn vào nó từ một quan điểm mới.
Sulthan

2
Tôi tiếp tục nhấp vào nút lên, nhưng tôi chỉ có thể cho bạn một điểm. Chủ nhân hiện tại của tôi quá thiển cận khi để chúng tôi thực sự thực hiện các bài kiểm tra đơn vị, và tôi vẫn được hưởng lợi từ việc viết mã một cách có ý thức như thể tôi đang làm việc theo hướng kiểm tra.
AmericanUmlaut

59

Có (dường như) các lực lượng đối lập trong chơi.

  • Một mặt, bạn muốn thực thi đóng gói
  • Mặt khác, bạn muốn có thể kiểm tra phần mềm

Những người đề xuất giữ tất cả 'chi tiết thực hiện' riêng tư thường được thúc đẩy bởi mong muốn duy trì đóng gói. Tuy nhiên, giữ mọi thứ bị khóa và không có sẵn là một cách tiếp cận bị hiểu sai về đóng gói. Nếu giữ mọi thứ không có sẵn là mục tiêu cuối cùng, mã được đóng gói thực sự duy nhất sẽ là:

static void Main(string[] args)

Là đồng nghiệp của bạn đề xuất làm cho điều này trở thành điểm truy cập duy nhất trong mã của bạn? Tất cả các mã khác có thể được truy cập bởi người gọi bên ngoài?

Khó khăn. Sau đó, những gì làm cho nó ổn để làm cho một số phương pháp công khai? Cuối cùng, đó có phải là một quyết định thiết kế chủ quan không?

Không hẳn. Những gì có xu hướng hướng dẫn các lập trình viên, thậm chí ở mức độ vô thức, một lần nữa, là khái niệm đóng gói. Bạn cảm thấy an toàn khi phơi bày một phương thức công khai khi nó bảo vệ đúng bất biến của nó .

Tôi không muốn để lộ một phương pháp tư nhân mà không bảo vệ bất biến của nó, nhưng thường bạn có thể sửa đổi nó để nó không bảo vệ bất biến của nó, và sau đó tiếp xúc với công chúng (tất nhiên, với TDD, bạn làm điều đó cách khác xung quanh).

Mở một API để kiểm tra là một điều tốt , bởi vì những gì bạn thực sự đang làm là áp dụng Nguyên tắc Mở / Đóng .

Nếu bạn chỉ có một người gọi API của mình, bạn sẽ không biết API của mình thực sự linh hoạt như thế nào. Rất có thể, nó khá không linh hoạt. Các thử nghiệm hoạt động như một khách hàng thứ hai, cung cấp cho bạn thông tin phản hồi có giá trị về tính linh hoạt của API của bạn .

Do đó, nếu các thử nghiệm cho thấy bạn nên mở API của mình, thì hãy thực hiện; nhưng duy trì sự đóng gói, không phải bằng cách che giấu sự phức tạp, mà bằng cách phơi bày sự phức tạp một cách không an toàn.


3
+1 Bạn cảm thấy an toàn khi phơi bày một phương thức công khai khi nó bảo vệ đúng các bất biến của nó.
shambulator

1
Tôi loooooove câu trả lời của bạn! :) Đã bao nhiêu lần tôi nghe: "Đóng gói là quá trình để riêng tư một số phương thức và thuộc tính". Nó giống như nói khi bạn lập trình theo hướng đối tượng, điều này là do bạn lập trình với các đối tượng. :( Tôi không ngạc nhiên khi một câu trả lời rõ ràng đến từ bậc thầy về tiêm chích phụ thuộc. Tôi chắc chắn sẽ đọc một vài lần câu trả lời của bạn để khiến tôi mỉm cười về mã di sản cũ nghèo nàn của mình.
Samuel

Tôi đã thêm một vài điều chỉnh cho câu trả lời của bạn. Bên cạnh đó, tôi đã đọc bài viết Đóng gói thuộc tính của bạn và lý do thuyết phục duy nhất tôi từng nghe khi sử dụng Thuộc tính tự động là việc thay đổi biến công khai thành thuộc tính công cộng phá vỡ tính tương thích nhị phân; hiển thị nó dưới dạng Thuộc tính tự động ngay từ đầu cho phép xác thực hoặc chức năng nội bộ khác được thêm vào sau mà không vi phạm mã máy khách.
Robert Harvey

21

Có vẻ như bạn đang nói về tiêm phụ thuộc . Nó thực sự phổ biến và IMO, khá cần thiết cho khả năng kiểm tra.

Để giải quyết câu hỏi rộng hơn về việc liệu có nên sửa đổi mã chỉ để làm cho nó có thể kiểm tra được hay không, hãy nghĩ về nó theo cách này: mã có nhiều trách nhiệm, bao gồm a) được thực thi, b) được đọc bởi con người và c) được thử nghiệm. Cả ba đều quan trọng và nếu mã của bạn không hoàn thành cả ba trách nhiệm, thì tôi sẽ nói đó không phải là mã tốt. Vì vậy, sửa đổi đi!


DI không phải là câu hỏi lớn (tôi chỉ cố gắng đưa ra một ví dụ), vấn đề là liệu có ổn không khi cung cấp một phương pháp khác ban đầu không được tạo ra, chỉ nhằm mục đích thử nghiệm.
liortal

4
Tôi biết. Tôi nghĩ rằng tôi đã giải quyết vấn đề chính của bạn trong đoạn thứ hai của tôi, với một "có".
Jason Swett

13

Đó là một chút vấn đề của con gà và quả trứng.

Một trong những lý do lớn nhất khiến việc kiểm tra mã của bạn tốt là vì nó cho phép bạn cấu trúc lại một cách không sợ hãi. Nhưng bạn đang ở trong một tình huống mà bạn cần cấu trúc lại mã để có được phạm vi kiểm tra tốt! Và đồng nghiệp của bạn là sợ hãi.

Tôi thấy quan điểm của đồng nghiệp của bạn. Bạn có mã mà (có lẽ là) hoạt động, và nếu bạn đi và cấu trúc lại nó - vì bất kỳ lý do gì - có nguy cơ bạn sẽ phá vỡ nó.

Nhưng nếu đây là mã được dự kiến ​​sẽ có bảo trì và sửa đổi liên tục, thì bạn sẽ gặp rủi ro đó mỗi khi bạn thực hiện bất kỳ công việc nào trên đó. Và tái cấu trúc ngay bây giờ và nhận được một số bảo hiểm thử nghiệm ngay bây giờ sẽ cho phép bạn chấp nhận rủi ro đó, trong các điều kiện được kiểm soát và đưa mã vào trạng thái tốt hơn để sửa đổi trong tương lai.

Vì vậy, tôi sẽ nói, trừ khi cơ sở mã cụ thể này khá tĩnh và dự kiến ​​sẽ không có công việc quan trọng nào được thực hiện trong tương lai, rằng những gì bạn muốn làm là thực hành kỹ thuật tốt.

Tất nhiên, cho dù đó là thực hành kinh doanh tốt hay không là toàn bộ 'sâu bọ ..


Một mối quan tâm quan trọng. Thông thường, tôi làm IDE hỗ trợ tái cấu trúc một cách tự do vì cơ hội phá vỡ bất cứ thứ gì có rất thấp. Nếu tái cấu trúc có liên quan nhiều hơn, tôi chỉ làm điều đó khi tôi cần thay đổi mã. Để thay đổi công cụ, bạn cần thử nghiệm để giảm rủi ro, do đó bạn thậm chí có được giá trị kinh doanh.
Hans-Peter Störr

Vấn đề là: chúng tôi muốn giữ mã cũ bởi vì chúng tôi muốn giao trách nhiệm cho đối tượng truy vấn cho hội đồng hiện tại hoặc vì chúng tôi không muốn thêm một số thuộc tính công cộng hoặc thay đổi chữ ký phương thức? Tôi nghĩ rằng mã vi phạm SRP và vì lý do này, nó nên được tái cấu trúc cho dù các đồng nghiệp có sợ thế nào. Chắc chắn nếu đây là API công khai được sử dụng bởi nhiều người dùng, bạn sẽ phải suy nghĩ về một chiến lược như triển khai mặt tiền hoặc bất cứ điều gì giúp bạn cấp cho mã cũ một giao diện ngăn chặn quá nhiều thay đổi.
Samuel

7

Đây có thể chỉ là một sự khác biệt trong nhấn mạnh từ các câu trả lời khác, nhưng tôi nói rằng mã không nên được cấu trúc lại một cách nghiêm ngặt để cải thiện khả năng kiểm tra. Khả năng kiểm tra là rất quan trọng để bảo trì, nhưng bản thân khả năng kiểm tra không phải là kết thúc. Như vậy, tôi sẽ trì hoãn mọi tái cấu trúc như vậy cho đến khi bạn có thể dự đoán rằng mã này sẽ cần bảo trì để tiếp tục kết thúc kinh doanh.

Tại thời điểm mà bạn xác định rằng mã này sẽ yêu cầu một số bảo trì, đó sẽ là thời điểm tốt để cấu trúc lại khả năng kiểm tra. Tùy thuộc vào trường hợp kinh doanh của bạn, có thể là một giả định hợp lệ rằng tất cả các mã cuối cùng sẽ yêu cầu một số bảo trì, trong trường hợp đó, sự khác biệt tôi rút ra với các câu trả lời khác ở đây ( ví dụ câu trả lời của Jason Swett ) sẽ biến mất.

Tóm lại: khả năng kiểm tra một mình không phải là (IMO) một lý do đủ để cấu trúc lại một cơ sở mã. Khả năng kiểm tra có một vai trò có giá trị trong việc cho phép bảo trì trên cơ sở mã, nhưng đó là một yêu cầu kinh doanh để sửa đổi chức năng mã của bạn, điều này sẽ thúc đẩy quá trình tái cấu trúc của bạn. Nếu không có yêu cầu kinh doanh như vậy, có lẽ tốt hơn là làm việc trên một cái gì đó mà khách hàng của bạn sẽ quan tâm.

(Mã mới, tất nhiên, đang được duy trì tích cực, vì vậy nó nên được viết để có thể kiểm tra được.)


2

Tôi nghĩ rằng đồng nghiệp của bạn là sai.

Những người khác đã đề cập đến lý do tại sao đây là một điều tốt, nhưng miễn là bạn được đưa ra trước để làm điều này, bạn sẽ ổn thôi.

Lý do cho sự cảnh báo này là việc thực hiện bất kỳ thay đổi nào đối với mã đều phải trả giá bằng mã phải được kiểm tra lại. Tùy thuộc vào những gì bạn làm, công việc kiểm tra này thực sự có thể là một nỗ lực lớn của chính nó.

Không nhất thiết là nơi bạn đưa ra quyết định tái cấu trúc so với làm việc trên các tính năng mới sẽ có lợi cho công ty / khách hàng của bạn.


2

Tôi đã sử dụng các công cụ bao phủ mã như một phần của kiểm tra đơn vị để kiểm tra xem tất cả các đường dẫn qua mã có được thực hiện hay không. Là một lập trình viên / kiểm thử viên khá giỏi, tôi thường bao gồm 80-90% các đường dẫn mã.

Khi tôi nghiên cứu các đường dẫn chưa được khám phá và nỗ lực cho một số trong số chúng, đó là khi tôi phát hiện ra các lỗi như các trường hợp lỗi sẽ "không bao giờ xảy ra". Vì vậy, có, sửa đổi mã và kiểm tra phạm vi kiểm tra làm cho mã tốt hơn.


2

Vấn đề của bạn ở đây là các công cụ kiểm tra của bạn là tào lao. Bạn sẽ có thể giả định đối tượng đó và gọi phương thức thử nghiệm của mình mà không thay đổi nó - bởi vì trong khi ví dụ đơn giản này thực sự đơn giản và dễ sửa đổi, điều gì xảy ra khi bạn có một thứ gì đó phức tạp hơn nhiều.

Rất nhiều người đã sửa đổi mã của họ để giới thiệu IoC, DI và các lớp dựa trên giao diện chỉ đơn giản là cho phép thử nghiệm đơn vị bằng cách sử dụng các công cụ kiểm tra đơn vị và thử nghiệm yêu cầu những thay đổi mã này. Tôi không gầy gò là một điều lành mạnh, không phải khi bạn thấy mã khá đơn giản và đơn giản biến thành một cơn ác mộng của các tương tác phức tạp được thúc đẩy hoàn toàn bởi nhu cầu làm cho mọi phương thức lớp hoàn toàn tách rời khỏi mọi thứ khác . Và để thêm sự xúc phạm đến thương tích, sau đó chúng tôi có nhiều tranh luận về việc liệu các phương pháp riêng tư có nên được thử nghiệm đơn vị hay không! (tất nhiên họ nên, cái gì '

Vấn đề, tất nhiên, là trong bản chất của công cụ kiểm tra sau đó.

Có những công cụ tốt hơn hiện có có thể đưa những thay đổi thiết kế này lên giường mãi mãi. Microsoft có Fakes (nee Moles) cho phép bạn khai thác các đối tượng cụ thể, bao gồm cả các đối tượng tĩnh, do đó bạn không còn cần phải thay đổi mã của mình để phù hợp với công cụ. Trong trường hợp của bạn, nếu bạn đã sử dụng Fakes, bạn sẽ thay thế cuộc gọi GetTypes bằng cuộc gọi của bạn đã trả về dữ liệu kiểm tra hợp lệ và không hợp lệ - điều này khá quan trọng, thay đổi được đề xuất của bạn hoàn toàn không cung cấp cho điều đó.

Để trả lời: đồng nghiệp của bạn đúng, nhưng có thể vì những lý do sai. Không thay đổi mã để kiểm tra, thay đổi công cụ kiểm tra của bạn (hoặc toàn bộ chiến lược kiểm tra của bạn để có các bài kiểm tra đơn vị theo kiểu tích hợp hơn thay vì kiểm tra chi tiết như vậy).

Martin Fowler có một cuộc thảo luận xung quanh lĩnh vực này trong bài viết của mình Mocks không Stub


1

Một thực hành tốt và phổ biến là sử dụng Nhật ký kiểm tra và gỡ lỗi đơn vị . Kiểm tra đơn vị đảm bảo rằng nếu bạn thực hiện thêm bất kỳ thay đổi nào trong chương trình, chức năng cũ của bạn sẽ không bị phá vỡ. Nhật ký gỡ lỗi có thể giúp bạn theo dõi chương trình trong thời gian chạy.
Đôi khi nó xảy ra rằng thậm chí xa hơn là chúng ta cần phải có một cái gì đó chỉ cho mục đích thử nghiệm. Nó không phải là bất thường để thay đổi mã cho việc này. Nhưng cần cẩn thận để mã sản xuất không bị ảnh hưởng vì điều này. Trong C ++ và C, điều này đạt được bằng cách sử dụng MACRO , là thực thể thời gian biên dịch. Sau đó, mã kiểm tra hoàn toàn không xuất hiện trong môi trường sản xuất. Không biết nếu điều khoản đó có trong C #.
Ngoài ra khi bạn thêm mã kiểm tra vào chương trình của mình, nó sẽ hiển thị rõ ràngrằng phần mã này được thêm vào cho một số mục đích thử nghiệm. Hoặc nếu không, nhà phát triển cố gắng hiểu mã chỉ đơn giản là sẽ đổ mồ hôi vào phần mã đó.


1

Có một số khác biệt nghiêm trọng giữa các ví dụ của bạn. Trong trường hợp DoSomethingOnAllTypes(), có một hàm ý do somethingáp dụng cho các loại trong lắp ráp hiện tại. Nhưng DoSomething(Assembly asm)rõ ràng chỉ ra rằng bạn có thể vượt qua bất kỳ lắp ráp cho nó.

Lý do tôi chỉ ra điều này là rất nhiều bước phụ thuộc chỉ để thử nghiệm ngoài giới hạn của đối tượng ban đầu. Tôi biết bạn đã nói " không" biến mọi thứ thành công khai " ", nhưng đó là một trong những lỗi lớn nhất của mô hình đó, theo sát điều này: mở các phương thức của đối tượng để sử dụng chúng không nhằm mục đích.


0

Câu hỏi của bạn không có nhiều bối cảnh trong đó đồng nghiệp của bạn tranh luận để có chỗ cho sự đầu cơ

"Thực hành xấu" hay không phụ thuộc vào cách thứcthời điểm thay đổi được thực hiện.

Trong ý kiến ​​của tôi, ví dụ của bạn để trích xuất một phương thức DoSomething(types)là ok.

Nhưng tôi đã thấy mã không ổn như thế này:

public void DoSomethingOnAllTypes()
{
  var types = (!testmode) 
      ? Assembly.GetExecutingAssembly().GetTypes() 
      : getTypesFromTestgenerator();

  foreach (var currentType in types)
  {
     if (!testmode)
     {
        // do something with this type that made the unittest fail and should be skipped.
     }
     // do something with this type (e.g: read it's attributes, process, etc).
  }
}

Những thay đổi này làm cho mã trở nên khó hiểu hơn vì bạn đã tăng số lượng đường dẫn mã có thể.

Ý tôi là gì với cách thứcthời gian :

nếu bạn có một triển khai hoạt động và vì mục đích "triển khai các khả năng kiểm tra", bạn đã thực hiện các thay đổi thì bạn phải kiểm tra lại ứng dụng của mình vì bạn có thể đã phá vỡ DoSomething()phương pháp của mình .

Việc if (!testmode)hiểu và kiểm tra có nhiều khác biệt hơn so với phương pháp trích xuất.

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.