Việc xây dựng các đối tượng với các tham số null trong các bài kiểm tra đơn vị có ổn không?


37

Tôi bắt đầu viết một số bài kiểm tra đơn vị cho dự án hiện tại của tôi. Tôi thực sự không có kinh nghiệm với nó mặc dù. Trước tiên tôi muốn hoàn toàn "hiểu", vì vậy hiện tại tôi không sử dụng khung IoC của mình cũng như thư viện chế giễu.

Tôi đã tự hỏi nếu có bất cứ điều gì sai khi cung cấp các đối số null cho các hàm tạo của các đối tượng trong các thử nghiệm đơn vị. Hãy để tôi cung cấp một số mã ví dụ:

public class CarRadio
{...}


public class Motor
{
    public void SetSpeed(float speed){...}
}


public class Car
{
    public Car(CarRadio carRadio, Motor motor){...}
}


public class SpeedLimit
{
    public bool IsViolatedBy(Car car){...}
}

Một ví dụ về mã xe khác (TM), chỉ còn những phần quan trọng đối với câu hỏi. Bây giờ tôi đã viết một bài kiểm tra như thế này:

public class SpeedLimitTest
{
    public void TestSpeedLimit()
    {
        Motor motor = new Motor();
        motor.SetSpeed(10f);
        Car car = new Car(null, motor);

        SpeedLimit speedLimit = new SpeedLimit();
        Assert.IsTrue(speedLimit.IsViolatedBy(car));
    }
}

Bài kiểm tra chạy tốt. SpeedLimitcần một Carvới một Motorđể làm điều của nó. Nó không quan tâm đến một CarRadiochút nào, vì vậy tôi đã cung cấp null cho điều đó.

Tôi tự hỏi nếu một đối tượng cung cấp chức năng chính xác mà không được xây dựng đầy đủ là vi phạm SRP hoặc mùi mã. Tôi có cảm giác dai dẳng như vậy, nhưng speedLimit.IsViolatedBy(motor)cũng không cảm thấy đúng - giới hạn tốc độ bị vi phạm bởi ô tô chứ không phải xe máy. Có lẽ tôi chỉ cần một quan điểm khác cho các bài kiểm tra đơn vị so với mã làm việc, bởi vì toàn bộ ý định là chỉ kiểm tra một phần của toàn bộ.

Là xây dựng các đối tượng với null trong đơn vị kiểm tra một mùi mã?


24
Một chút lạc đề, nhưng Motorcó lẽ không nên có một speedchút nào. Nó nên có một throttlevà tính toán torquedựa trên hiện tại rpmthrottle. Đó là công việc của chiếc xe là sử dụng Transmissionđể tích hợp tốc độ đó vào tốc độ hiện tại và biến nó thành rpmtín hiệu trở lại Motor... Nhưng tôi đoán, dù sao bạn cũng không ở trong đó vì chủ nghĩa hiện thực, phải không?
cmaster

17
Mùi mã là nhà xây dựng của bạn mất null mà không phàn nàn ở nơi đầu tiên. Nếu bạn nghĩ rằng điều này có thể chấp nhận được (bạn có thể có lý do cho việc này): hãy tiếp tục, kiểm tra nó!
Tommy

4
Hãy xem xét mẫu Null-Object . Nó thường đơn giản hóa mã, đồng thời làm cho nó an toàn NPE.
Bakuriu

17
Xin chúc mừng : bạn đã kiểm tra rằng với một nullđài phát thanh, giới hạn tốc độ được tính toán chính xác. Bây giờ bạn có thể muốn tạo một bài kiểm tra để xác nhận giới hạn tốc độ bằng radio; chỉ trong trường hợp hành vi khác đi ...
Matthieu M.

3
Không chắc chắn về ví dụ này, nhưng đôi khi thực tế là bạn chỉ muốn kiểm tra một nửa lớp là một manh mối cho thấy một cái gì đó nên được chia tay
Owen

Câu trả lời:


70

Trong trường hợp ví dụ trên, điều hợp lý là a Carcó thể tồn tại mà không có a CarRadio. Trong trường hợp đó, đôi khi tôi nói rằng không chỉ có thể chấp nhận chuyển thành null CarRadio, tôi còn nói rằng điều đó là bắt buộc. Các thử nghiệm của bạn cần đảm bảo rằng việc triển khai Carlớp là mạnh mẽ và không đưa ra các ngoại lệ con trỏ null khi không có CarRadiomặt.

Tuy nhiên, hãy lấy một ví dụ khác - hãy xem xét a SteeringWheel. Chúng ta hãy giả sử rằng Carphải có một SteeringWheel, nhưng bài kiểm tra tốc độ không thực sự quan tâm đến nó. Trong trường hợp này, tôi sẽ không vượt qua null SteeringWheelvì điều này sau đó sẽ đẩy Carlớp vào những nơi mà nó không được thiết kế để đi. Trong trường hợp này, bạn nên tạo ra một số loại trong DefaultSteeringWheelđó (để tiếp tục ẩn dụ) được khóa theo một đường thẳng.


44
Và đừng quên viết một bài kiểm tra đơn vị để kiểm tra xem Carkhông thể xây dựng được nếu không có SteeringWheel...
cmaster 20/03/18

13
Tôi có thể đang thiếu một cái gì đó, nhưng nếu có thể và thậm chí phổ biến để tạo ra một thứ Carkhông có CarRadio, thì sẽ không có ý nghĩa gì khi tạo một hàm tạo không cần phải truyền vào và không phải lo lắng về một null rõ ràng nào cả ?
một Earwig

16
Xe tự lái có thể không có tay lái. Chỉ cần nói. ☺
BlackJack

1
@James_Parsons Tôi quên thêm rằng đó là về một lớp không có kiểm tra null vì nó chỉ được sử dụng nội bộ và luôn nhận được các tham số không null. Các phương pháp khác Carsẽ ném NRE bằng null CarRadio, nhưng mã có liên quan SpeedLimitsẽ không. Trong trường hợp câu hỏi như được đăng bây giờ, bạn sẽ đúng và tôi thực sự sẽ làm điều đó.
R. Schmitz

2
@mtraceur Cách mọi người cho rằng lớp cho phép xây dựng với đối số null có nghĩa là null là một tùy chọn hợp lệ khiến tôi nhận ra rằng có lẽ tôi nên kiểm tra null ở đó. Vấn đề thực tế tôi sẽ có sau đó được bao phủ bởi rất nhiều câu trả lời hay, thú vị, được viết tốt ở đây mà tôi không muốn làm hỏng và điều đó đã giúp tôi. Đồng thời chấp nhận câu trả lời này ngay bây giờ vì tôi không muốn bỏ mặc những điều còn dang dở và đó là điều tuyệt vời nhất (mặc dù tất cả chúng đều hữu ích).
R. Schmitz

23

Là xây dựng các đối tượng với null trong đơn vị kiểm tra một mùi mã?

Vâng. Lưu ý định nghĩa C2 về mùi mã : "CodeSmell là một gợi ý rằng có thể có gì đó không đúng, không chắc chắn."

Bài kiểm tra chạy tốt. SpeedLimit cần một chiếc xe có động cơ để thực hiện công việc của mình. Nó hoàn toàn không quan tâm đến một CarRadio, vì vậy tôi đã cung cấp null cho điều đó.

Nếu bạn đang cố chứng minh rằng điều speedLimit.IsViolatedBy(car)đó không phụ thuộc vào CarRadiođối số được truyền cho hàm tạo, thì có lẽ sẽ rõ ràng hơn với người bảo trì trong tương lai để làm cho ràng buộc đó rõ ràng bằng cách đưa ra một phép thử hai lần mà không kiểm tra được nếu nó được gọi.

Nếu, nhiều khả năng, bạn chỉ đang cố gắng tự cứu mình công việc tạo ra một thứ CarRadiomà bạn biết không được sử dụng trong trường hợp này, bạn nên chú ý rằng

  • đây là vi phạm đóng gói (bạn đang để thực tế CarRadiokhông được sử dụng rò rỉ vào thử nghiệm)
  • bạn đã phát hiện ra ít nhất một trường hợp bạn muốn tạo Carmà không chỉ địnhCarRadio

Tôi rất nghi ngờ rằng mã đang cố nói với bạn rằng bạn muốn một hàm tạo giống như

public Car(IMotor motor) {...}

và sau đó nhà xây dựng đó có thể quyết định phải làm gì với thực tế là không cóCarRadio

public Car(IMotor motor) {
    this(null, motor);
}

hoặc là

public Car(IMotor motor) {
    this( new ICarRadio() {...} , motor);
}

hoặc là

public Car(IMotor motor) {
    this( new DefaultRadio(), motor);
}

v.v.

Đó là về việc chuyển null sang thứ khác trong khi kiểm tra SpeedLimit. Bạn có thể thấy rằng bài kiểm tra có tên là "SpeedLimitTest" và Assert duy nhất đang kiểm tra một phương pháp của SpeedLimit.

Đó là một mùi thú vị thứ hai - để kiểm tra SpeedLimit, bạn phải chế tạo toàn bộ chiếc xe xung quanh một chiếc radio, mặc dù điều duy nhất mà kiểm tra SpeedLimit có thể quan tâm là tốc độ .

Đây có thể là một gợi ý SpeedLimit.isViolatedBynên chấp nhận giao diện vai trò mà xe thực hiện , thay vì yêu cầu toàn bộ xe.

interface IHaveSpeed {
    float speed();
}

class Car implements IHaveSpeed {...}

IHaveSpeedlà một cái tên thực sự tệ hại; hy vọng ngôn ngữ tên miền của bạn sẽ có một sự cải thiện.

Với giao diện đó, thử nghiệm của bạn SpeedLimitcó thể đơn giản hơn nhiều

public void TestSpeedLimit()
{
    IHaveSpeed somethingTravelingQuickly = new IHaveSpeed {
        float speed() { return 10f; }
    }

    SpeedLimit speedLimit = new SpeedLimit();
    Assert.IsTrue(speedLimit.IsViolatedBy(somethingTravelingQuickly));
}

Trong ví dụ cuối cùng của bạn, tôi cho rằng bạn phải tạo một lớp giả thực hiện IHaveSpeedgiao diện như (ít nhất là trong c #), bạn sẽ không thể tự khởi tạo một giao diện.
Ryan Searle

1
Điều đó đúng - cách đánh vần hiệu quả sẽ phụ thuộc vào hương vị của Blub mà bạn đang sử dụng cho các bài kiểm tra của mình.
VoiceOfUnreason

18

Là xây dựng các đối tượng với null trong đơn vị kiểm tra một mùi mã?

Không

Không có gì. Ngược lại, nếu NULLlà một giá trị có sẵn dưới dạng đối số (một số ngôn ngữ có thể có các cấu trúc không cho phép truyền con trỏ null), bạn nên kiểm tra nó.

Một câu hỏi khác là những gì xảy ra khi bạn vượt qua NULL. Nhưng đó chính xác là những gì một bài kiểm tra đơn vị nói về: đảm bảo một kết quả được xác định đang xảy ra khi cho mỗi đầu vào có thể. Và NULL một đầu vào tiềm năng, vì vậy bạn nên có một kết quả xác định và bạn nên có một bài kiểm tra cho điều đó.

Vì vậy, nếu Xe của bạn được cho là có thể xây dựng mà không có radio, bạn nên viết một bài kiểm tra cho điều đó. Nếu không và cần phải đưa ra một ngoại lệ, bạn nên viết một bài kiểm tra cho điều đó.

Dù bằng cách nào, có bạn nên viết một bài kiểm tra cho NULL. Nó sẽ là một mùi mã nếu bạn để nó ra. Điều đó có nghĩa là bạn có một nhánh mã chưa được kiểm tra mà bạn vừa giả sử sẽ không có lỗi.


Xin lưu ý rằng tôi đồng ý với các câu trả lời khác rằng mã của bạn đang được kiểm tra có thể được cải thiện. Tuy nhiên, nếu mã được cải tiến có các tham số là con trỏ, thì việc chuyển NULLngay cả đối với mã được cải tiến là một thử nghiệm tốt. Tôi muốn có một bài kiểm tra để cho thấy những gì xảy ra khi tôi vượt qua NULLnhư một động cơ. Đó không phải là mùi mã, ngược lại với mùi mã. Đó là thử nghiệm.


Có vẻ như bạn đang viết bài kiểm tra Cartại đây. Mặc dù tôi không thấy bất cứ điều gì tôi cho là một tuyên bố sai, câu hỏi là về thử nghiệm SpeedLimit.
R. Schmitz

@ R.Schmitz Không chắc ý của bạn là gì. Tôi đã trích dẫn câu hỏi, đó là về việc chuyển NULLđến nhà xây dựng của Car.
nvoigt

1
Đó là về nếu chuyển null cho một cái gì đó khác trong khi thử nghiệmSpeedLimit . Bạn có thể thấy rằng bài kiểm tra có tên là "SpeedLimitTest" và cách duy nhất Assertlà kiểm tra một phương thức SpeedLimit. Thử nghiệm Car- ví dụ "nếu Xe của bạn được cho là có thể xây dựng mà không có radio, bạn nên viết thử nghiệm cho điều đó" - sẽ xảy ra trong một thử nghiệm khác.
R. Schmitz

7

Cá nhân tôi sẽ do dự để tiêm null. Trong thực tế, bất cứ khi nào tôi tiêm các phụ thuộc vào một hàm tạo, một trong những điều đầu tiên tôi làm là đưa ra một ArgumentNullException nếu các phép tiêm đó là null. Bằng cách đó tôi có thể sử dụng chúng một cách an toàn trong lớp của mình mà không cần hàng tá kiểm tra null. Đây là một mô hình rất phổ biến. Lưu ý việc sử dụng chỉ đọc để đảm bảo các phụ thuộc được đặt trong hàm tạo không thể được đặt thành null (hoặc bất cứ thứ gì khác) sau đó:

class Car
{
   private readonly ICarRadio _carRadio;
   private readonly IMotor _motor;

   public Car(ICarRadio carRadio, IMotor motor)
   {
      if (carRadio == null)
         throw new ArgumentNullException(nameof(carRadio));

      if (motor == null)
         throw new ArgumentNullException(nameof(motor));

      _carRadio = carRadio;
      _motor = motor;
   }
}

Với những điều trên, có lẽ tôi có thể có một hoặc hai bài kiểm tra đơn vị, nơi tôi tiêm null, chỉ để đảm bảo kiểm tra null của tôi hoạt động như mong đợi.

Có nói rằng, điều này trở thành một vấn đề, một khi bạn làm hai điều:

  1. Chương trình để giao diện thay vì các lớp.
  2. Sử dụng khung mô phỏng (như Moq cho C #) để thêm các phụ thuộc bị giả thay vì null.

Vì vậy, ví dụ của bạn, bạn có:

interface ICarRadio
{
   bool Tune(float frequency);
}

interface Motor
{
   bool SetSpeed(float speed);
}

...
var car = new Car(new Moq<ICarRadio>().Object, new Moq<IMotor>().Object);

Thông tin chi tiết về việc sử dụng Moq có thể được tìm thấy ở đây .

Tất nhiên, nếu bạn muốn hiểu nó mà không cần chế giễu trước, bạn vẫn có thể làm điều đó, miễn là bạn đang sử dụng giao diện. Đơn giản chỉ cần tạo một CarRadio và Motor giả như sau và thay vào đó:

class DummyCarRadio : ICarRadio
{
   ...
}
class DummyMotor : IMotor
{
   ...
}

...
var car = new Car(new DummyCarRadio(), new DummyMotor())

3
Đây là câu trả lời tôi sẽ viết
Liath 20/03/18

1
Tôi thậm chí sẽ đi xa hơn để nói rằng các đối tượng giả cũng phải thực hiện hợp đồng của họ. Nếu IMotorcó một getSpeed()phương thức, thì DummyMotorcũng cần phải trả lại tốc độ đã sửa đổi sau khi thành công setSpeed.
ComFalet

1

Nó không chỉ ổn, đó là một kỹ thuật phá vỡ sự phụ thuộc nổi tiếng để lấy mã kế thừa vào một khai thác thử nghiệm. (Xem phần Làm việc hiệu quả với Bộ luật kế thừa của Michael Feathers.)

Nó có mùi không? Tốt...

Các thử nghiệm không có mùi. Bài kiểm tra đang làm công việc của nó. Việc khai báo đối tượng này có thể là null và mã được kiểm tra vẫn hoạt động chính xác.

Mùi là trong thực hiện. Bằng cách khai báo một đối số hàm tạo, bạn đang khai báo lớp này, lớp này cần điều này để hoạt động đúng chức năng. Rõ ràng, đó là không đúng sự thật. Bất cứ điều gì không đúng sự thật là một chút mùi.


1

Hãy lùi một bước về các nguyên tắc đầu tiên.

Một bài kiểm tra đơn vị kiểm tra một số khía cạnh của một lớp duy nhất.

Tuy nhiên, sẽ có các hàm tạo hoặc các phương thức lấy các lớp khác làm tham số. Cách bạn xử lý những điều này sẽ khác nhau tùy theo từng trường hợp nhưng thiết lập nên tối thiểu vì chúng tiếp tuyến với mục tiêu. Có thể có các kịch bản trong đó việc truyền tham số NULL là hoàn toàn hợp lệ - chẳng hạn như một chiếc xe không có radio.

Tôi giả sử ở đây, rằng chúng tôi chỉ đang thử nghiệm đường đua để bắt đầu (để tiếp tục chủ đề xe hơi), nhưng bạn cũng có thể muốn chuyển NULL sang các tham số khác để kiểm tra xem bất kỳ mã phòng thủ và cơ chế xử lý ngoại lệ nào đang hoạt động như cần thiết.

Càng xa càng tốt. Tuy nhiên, nếu NULL không phải là một giá trị hợp lệ. Chà, ở đây, bạn đang ở trong thế giới âm u của những kẻ nhạo báng, cuống, người giả và người giả .

Nếu tất cả điều này có vẻ hơi nhiều, bạn thực sự đã sử dụng một hình nộm bằng cách chuyển NULL trong Trình tạo ô tô vì đó là một giá trị có khả năng sẽ không được sử dụng.

Điều sẽ trở nên rõ ràng khá nhanh, đó là bạn có khả năng bắn mã của bạn với nhiều cách xử lý NULL. Nếu bạn thay thế các tham số lớp bằng các giao diện, thì bạn cũng sẽ mở đường cho việc chế nhạo và DI vv, điều này làm cho những điều này trở nên đơn giản hơn để xử lý.


1
"Các bài kiểm tra đơn vị là để kiểm tra mã của một lớp duy nhất." Thở dài . Đó không phải là những bài kiểm tra đơn vị và theo hướng dẫn đó sẽ dẫn bạn vào các lớp học cồng kềnh hoặc các bài kiểm tra gây phản tác dụng, cản trở.
Ant P

3
không may coi "đơn vị" là một uyển ngữ cho "lớp" là một cách suy nghĩ rất phổ biến nhưng trong thực tế, tất cả những điều này là (a) khuyến khích các bài kiểm tra "rác, bỏ rác", hoàn toàn có thể làm suy yếu tính hợp lệ của toàn bộ bài kiểm tra bộ và (b) thực thi các ranh giới nên được tự do thay đổi, có thể làm tê liệt năng suất. Tôi ước nhiều người sẽ lắng nghe nỗi đau của họ và thách thức định kiến ​​này.
Ant P

3
Không có sự nhầm lẫn như vậy. Kiểm tra đơn vị hành vi, không phải đơn vị mã. Không có gì về khái niệm kiểm thử đơn vị - ngoại trừ trong ý tưởng giáo dục phần mềm phổ biến rộng rãi về kiểm thử phần mềm - ngụ ý rằng một đơn vị kiểm thử được kết hợp với khái niệm OOP khác biệt của một lớp. Và một quan niệm sai lầm rất tai hại đó là, quá.
Ant P

1
Tôi không chắc chính xác ý bạn là gì - Tôi đang tranh luận hoàn toàn ngược lại với những gì bạn đang ám chỉ. Stub / mock ranh giới cứng (nếu bạn hoàn toàn phải) và kiểm tra một đơn vị hành vi . Thông qua API công khai. API đó phụ thuộc vào những gì bạn đang xây dựng nhưng dấu chân phải ở mức tối thiểu. Việc thêm mã trừu tượng, đa hình hoặc mã tái cấu trúc sẽ không làm cho các thử nghiệm thất bại - "đơn vị trên mỗi lớp" mâu thuẫn với điều này và hoàn toàn làm giảm giá trị của các thử nghiệm ở vị trí đầu tiên.
Ant P

1
RobbieDee - Tôi đang nói về điều ngược lại chính xác. Hãy xem xét rằng bạn có thể là người có những quan niệm sai lầm phổ biến, xem xét lại những gì bạn coi là "đơn vị" và có thể tìm hiểu sâu hơn một chút về những gì Kent Beck thực sự đang nói khi nói về TDD. Điều mà hầu hết mọi người gọi là TDD bây giờ là sự quái dị sùng bái hàng hóa. youtube.com/watch?v=EZ05e7EMOLM
Ant P

1

Nhìn vào thiết kế của bạn, tôi sẽ đề nghị rằng một Carbao gồm một bộ Features. Một tính năng có thể là một CarRadiohoặc EntertainmentSystemcó thể bao gồm những thứ như a CarRadio, DvdPlayerv.v.

Vì vậy, ở mức tối thiểu, bạn sẽ phải xây dựng Carmột tập hợpFeatures . EntertainmentSystemCarRadiosẽ là người triển khai giao diện (Java, C #, v.v.) public [I|i]nterface Featurevà đây sẽ là một ví dụ của mẫu thiết kế hỗn hợp.

Do đó, bạn sẽ luôn luôn xây dựng một Carvới Featuresmột bài kiểm tra đơn vị sẽ không bao giờ thuyết minh mộtCar không có Features. Mặc dù bạn có thể xem xét việc chế nhạo một BasicTestCarFeatureSetbộ có chức năng tối thiểu cần thiết cho bài kiểm tra cơ bản nhất của bạn.

Chỉ là một vài suy nghĩ.

John

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.