Kiểm tra đơn vị trong một thế giới không có setter


23

Tôi không coi mình là một chuyên gia DDD nhưng, với tư cách là một kiến ​​trúc sư giải pháp, hãy cố gắng áp dụng các thực tiễn tốt nhất bất cứ khi nào có thể. Tôi biết có rất nhiều cuộc thảo luận xung quanh "phong cách" chuyên nghiệp và không có (công khai) trong DDD và tôi có thể thấy cả hai mặt của tranh luận. Vấn đề của tôi là tôi làm việc trong một nhóm có sự đa dạng về kỹ năng, kiến ​​thức và kinh nghiệm có nghĩa là tôi không thể tin rằng mọi nhà phát triển sẽ làm mọi thứ theo cách "đúng". Chẳng hạn, nếu các đối tượng miền của chúng ta được thiết kế sao cho việc thay đổi trạng thái bên trong của đối tượng được thực hiện bằng một phương thức nhưng cung cấp setters thuộc tính công cộng, chắc chắn sẽ có người đặt thuộc tính thay vì gọi phương thức. Sử dụng ví dụ này:

public class MyClass
{
    public Boolean IsPublished
    {
        get { return PublishDate != null; }
    }

    public DateTime? PublishDate { get; set; }

    public void Publish()
    {
        if (IsPublished)
            throw new InvalidOperationException("Already published.");

        PublishDate = DateTime.Today;

        Raise(new PublishedEvent());
    }
}

Giải pháp của tôi là làm cho các setters thuộc tính riêng tư, điều này là có thể bởi vì ORM mà chúng ta đang sử dụng để hydrat hóa các đối tượng sử dụng sự phản chiếu để nó có thể truy cập vào các setters riêng. Tuy nhiên, điều này trình bày một vấn đề khi cố gắng viết bài kiểm tra đơn vị. Ví dụ, khi tôi muốn viết một bài kiểm tra đơn vị xác minh yêu cầu mà chúng tôi không thể xuất bản lại, tôi cần chỉ ra rằng đối tượng đã được xuất bản. Tôi chắc chắn có thể làm điều này bằng cách gọi Xuất bản hai lần, nhưng sau đó thử nghiệm của tôi cho rằng Xuất bản được triển khai chính xác cho cuộc gọi đầu tiên. Điều đó có vẻ hơi mùi.

Hãy tạo kịch bản thực tế hơn một chút với mã sau:

public class Document
{
    public Document(String title)
    {
        if (String.IsNullOrWhiteSpace(title))
            throw new ArgumentException("title");

        Title = title;
    }

    public String ApprovedBy { get; private set; }
    public DateTime? ApprovedOn { get; private set; }
    public Boolean IsApproved { get; private set; }
    public Boolean IsPublished { get; private set; }
    public String PublishedBy { get; private set; }
    public DateTime? PublishedOn { get; private set; }
    public String Title { get; private set; }

    public void Approve(String by)
    {
        if (IsApproved)
            throw new InvalidOperationException("Already approved.");

        ApprovedBy = by;
        ApprovedOn = DateTime.Today;
        IsApproved = true;

        Raise(new ApprovedEvent(Title));
    }

    public void Publish(String by)
    {
        if (IsPublished)
            throw new InvalidOperationException("Already published.");

        if (!IsApproved)
            throw new InvalidOperationException("Cannot publish until approved.");

        PublishedBy = by;
        PublishedOn = DateTime.Today;
        IsPublished = true;

        Raise(new PublishedEvent(Title));
    }
}

Tôi muốn viết bài kiểm tra đơn vị xác minh:

  • Tôi không thể xuất bản trừ khi Tài liệu đã được phê duyệt
  • Tôi không thể xuất bản lại Tài liệu
  • Khi được xuất bản, các giá trị PublishBy và PublishOn được đặt đúng
  • Khi xuất hiện, PublishEvent được nâng lên

Không có quyền truy cập vào setters, tôi không thể đặt đối tượng vào trạng thái cần thiết để thực hiện các bài kiểm tra. Mở truy cập vào setters đánh bại mục đích ngăn chặn truy cập.

Làm thế nào để (có) bạn giải quyết (d) vấn đề này?


Tôi càng nghĩ về điều này, tôi càng nghĩ rằng toàn bộ vấn đề của bạn đang có các phương pháp với tác dụng phụ. Hay đúng hơn, một đối tượng bất biến có thể thay đổi. Trong thế giới DDD, bạn không nên trả về một đối tượng Tài liệu mới từ cả Phê duyệt và Xuất bản, thay vì cập nhật trạng thái bên trong của đối tượng này?
pdr

1
Câu hỏi nhanh, bạn đang sử dụng O / RM nào. Tôi là một fan hâm mộ lớn của EF nhưng việc tuyên bố setters là được bảo vệ sẽ khiến tôi hiểu sai một chút.
Michael Brown

Chúng tôi có một sự pha trộn ngay bây giờ vì sự phát triển phạm vi miễn phí mà tôi đã bị buộc tội gây lộn. Một số ADO.NET sử dụng AutoMapper để hydrat hóa từ DataReader, một vài mô hình Linq-SQL (sẽ là mô hình tiếp theo thay thế ) và một số mô hình EF mới.
SonOfPirate

Gọi Xuất bản hai lần không có mùi gì cả và là cách để làm điều đó.
Piotr Perak

Câu trả lời:


27

Tôi không thể đặt đối tượng vào trạng thái cần thiết để thực hiện các bài kiểm tra.

Nếu bạn không thể đặt đối tượng vào trạng thái cần thiết để thực hiện kiểm tra, thì bạn không thể đặt đối tượng vào trạng thái trong mã sản xuất, do đó không cần phải kiểm tra trạng thái đó . Rõ ràng, điều này không đúng trong trường hợp của bạn, bạn có thể đặt đối tượng của mình vào trạng thái cần thiết, chỉ cần gọi Phê duyệt.

  • Tôi không thể xuất bản trừ khi Tài liệu đã được phê duyệt: viết bài kiểm tra gọi xuất bản trước khi gọi phê duyệt gây ra lỗi đúng mà không thay đổi trạng thái đối tượng.

    void testPublishBeforeApprove() {
        doc = new Document("Doc");
        AssertRaises(doc.publish, ..., NotApprovedException);
    }
    
  • Tôi không thể xuất bản lại Tài liệu: viết một bài kiểm tra phê duyệt một đối tượng, sau đó gọi xuất bản một lần thành công, nhưng lần thứ hai gây ra lỗi đúng mà không thay đổi trạng thái đối tượng.

    void testRePublish() {
        doc = new Document("Doc");
        doc.approve();
        doc.publish();
        AssertRaises(doc.publish, ..., RepublishException);
    }
    
  • Khi được xuất bản, các giá trị PublishBy và PublishOn được đặt đúng: viết một bài kiểm tra gọi phê duyệt sau đó gọi xuất bản, khẳng định rằng trạng thái đối tượng thay đổi chính xác

    void testPublish() {
        doc = new Document("Doc");
        doc.approve();
        doc.publish();
        Assert(doc.PublishedBy, ...);
        ...
    }
    
  • Khi được xuất bản, PublishEvent được nâng lên: móc vào hệ thống sự kiện và đặt cờ để đảm bảo nó được gọi

Bạn cũng cần phải viết bài kiểm tra để phê duyệt.

Nói cách khác, đừng kiểm tra mối quan hệ giữa các trường nội bộ và IsPublished và IsApproved, bài kiểm tra của bạn sẽ khá mong manh nếu bạn làm điều đó vì thay đổi trường của bạn có nghĩa là thay đổi mã kiểm tra của bạn, vì vậy bài kiểm tra sẽ khá vô nghĩa. Thay vào đó, bạn nên kiểm tra mối quan hệ giữa các cuộc gọi của phương thức công khai, theo cách này, ngay cả khi bạn sửa đổi các trường bạn sẽ không cần phải sửa đổi thử nghiệm.


Khi phê duyệt phá vỡ, một số thử nghiệm phá vỡ. Bạn không còn kiểm tra một đơn vị mã, bạn đang kiểm tra việc thực hiện đầy đủ.
pdr

Tôi chia sẻ mối quan tâm của pdr đó là lý do tại sao tôi ngần ngại đi theo hướng này. Vâng, nó có vẻ sạch nhất, nhưng tôi không muốn có nhiều lý do một bài kiểm tra cá nhân có thể thất bại.
SonOfPirate

4
Tôi vẫn chưa thấy một bài kiểm tra đơn vị chỉ có thể thất bại vì một lý do duy nhất có thể. Ngoài ra, bạn có thể đặt các phần "thao tác trạng thái" của bài kiểm tra vào một setup()phương thức --- không phải bản thân bài kiểm tra.
Peter K.

12
Tại sao phụ thuộc vào một số approve()giòn, nhưng phụ thuộc vào một setApproved(true)cách nào đó không? approve()là một phụ thuộc hợp pháp trong các bài kiểm tra vì nó là một phụ thuộc trong các yêu cầu. Nếu sự phụ thuộc chỉ tồn tại trong các thử nghiệm, đó sẽ là một vấn đề khác.
Karl Bielefeldt

2
@pdr, bạn sẽ kiểm tra một lớp stack như thế nào? Bạn sẽ thử kiểm tra push()pop()phương pháp một cách độc lập?
Winston Ewert

2

Tuy nhiên, một cách tiếp cận khác là tạo một hàm tạo của lớp cho phép các thuộc tính bên trong được thiết lập trên khởi tạo:

 public Document(
  String approvedBy,
  DateTime? approvedOn,
  Boolean isApproved,
  Boolean isPublished,
  String publishedBy,
  DateTime? publishedOn,
  String title)
{
  ApprovedBy = approvedBy;
  ApprovedOn = approvedOn;
  IsApproved = isApproved;
  IsApproved = isApproved;
  PublishedBy = publishedBy;
  PublishedOn = publishedOn;
}

2
Điều này không có quy mô tốt cả. Đối tượng của tôi có thể có nhiều thuộc tính hơn với bất kỳ số nào trong số chúng có hoặc không có giá trị tại bất kỳ điểm đã cho nào trong vòng đời của đối tượng. Tôi theo nguyên tắc rằng các hàm tạo chứa các tham số cho các thuộc tính được yêu cầu cho đối tượng ở trạng thái ban đầu hợp lệ hoặc phụ thuộc mà đối tượng yêu cầu để hoạt động. Mục đích của các thuộc tính trong ví dụ là để nắm bắt trạng thái hiện tại khi đối tượng bị thao túng. Có một nhà xây dựng với mọi tài sản hoặc quá tải với các kết hợp khác nhau là một mùi rất lớn và, như tôi đã nói, không có quy mô.
SonOfPirate

Hiểu. Ví dụ của bạn không đề cập đến nhiều thuộc tính nữa và số trong ví dụ là "trên đỉnh" của việc có điều này như một cách tiếp cận hợp lệ. Dường như điều này đang nói với bạn điều gì đó về thiết kế của bạn: bạn không thể đặt đối tượng của mình vào bất kỳ trạng thái hợp lệ nào khi khởi tạo. Điều đó có nghĩa là bạn cần đặt nó vào trạng thái ban đầu hợp lệ và họ thao tác nó vào trạng thái phù hợp để kiểm tra. Điều đó ngụ ý câu trả lời của Lie Ryan là con đường để đi .
Peter K.

Ngay cả khi đối tượng có một tài sản và sẽ không bao giờ thay đổi giải pháp này là xấu. Điều gì ngăn cản bất cứ ai sử dụng nhà xây dựng này trong sản xuất? Làm thế nào bạn sẽ đánh dấu nhà xây dựng này [TestOnly]?
Piotr Perak

Tại sao nó xấu trong sản xuất? (Thực sự, tôi muốn biết). Đôi khi, cần phải tạo lại trạng thái chính xác của một đối tượng khi tạo ... không chỉ là một đối tượng ban đầu hợp lệ.
Peter K.

1
Vì vậy, trong khi điều đó giúp đưa đối tượng vào trạng thái ban đầu hợp lệ, việc kiểm tra hành vi của đối tượng tại đó tiến triển thông qua vòng đời của nó đòi hỏi đối tượng phải được thay đổi từ trạng thái ban đầu. OP của tôi phải thực hiện với việc kiểm tra các trạng thái bổ sung này khi bạn không thể đơn giản đặt thuộc tính để thay đổi trạng thái của đối tượng.
SonOfPirate

1

Một chiến lược là bạn kế thừa lớp (trong trường hợp này là Tài liệu) và viết các bài kiểm tra đối với lớp được kế thừa. Lớp kế thừa cho phép một số cách để thiết lập trạng thái đối tượng trong các thử nghiệm.

Trong chiến lược C # một có thể là làm cho setters nội bộ, sau đó phơi bày nội bộ để thử nghiệm dự án.

Bạn cũng có thể sử dụng API lớp như bạn mô tả ("Tôi chắc chắn có thể làm điều này bằng cách gọi Xuất bản hai lần"). Đây sẽ là cài đặt trạng thái đối tượng bằng cách sử dụng các dịch vụ công cộng của đối tượng, nó dường như không quá nặng mùi đối với tôi. Trong trường hợp ví dụ của bạn, đây có thể là cách tôi làm.


Tôi nghĩ về điều này như một giải pháp khả thi nhưng do dự để làm cho các thuộc tính của mình bị quá tải hoặc phơi bày các setters như được bảo vệ bởi vì nó có cảm giác như tôi đang mở đối tượng và phá vỡ đóng gói. Tôi nghĩ rằng làm cho các tài sản được bảo vệ chắc chắn là tốt hơn so với công khai hoặc thậm chí nội bộ / bạn bè. Tôi chắc chắn sẽ cung cấp cho phương pháp này nhiều suy nghĩ hơn. Thật đơn giản và hiệu quả. Đôi khi đó là cách tiếp cận tốt nhất. Nếu bất cứ ai không đồng ý, xin vui lòng thêm ý kiến ​​với các chi tiết cụ thể.
SonOfPirate

1

Để kiểm tra cách ly tuyệt đối các lệnh và truy vấn mà các đối tượng miền nhận được, tôi được sử dụng để cung cấp cho mỗi thử nghiệm một chuỗi tuần tự của đối tượng ở trạng thái mong đợi. Trong phần sắp xếp của bài kiểm tra, nó tải đối tượng để kiểm tra từ một tệp mà tôi đã chuẩn bị trước đó. Lúc đầu, tôi bắt đầu với việc tuần tự hóa nhị phân, nhưng json đã được chứng minh là dễ dàng hơn rất nhiều để bảo trì. Điều này tỏ ra hoạt động tốt, bất cứ khi nào sự cô lập tuyệt đối trong các thử nghiệm cung cấp giá trị thực tế.

chỉ chỉnh sửa một ghi chú, đôi khi lỗi tuần tự hóa JSON không thành công (như trong trường hợp đồ thị của đối tượng tuần hoàn, đó là một mùi, btw). Trong tình huống như vậy, tôi giải cứu để tuần tự hóa nhị phân. Đó là một chút thực dụng, nhưng hoạt động. :-)


Và làm thế nào để bạn chuẩn bị đối tượng ở trạng thái mong đợi nếu không có setter và bạn không muốn gọi nó là phương thức công khai để thiết lập nó?
Piotr Perak

Tôi đã viết một công cụ nhỏ cho việc đó. Nó tải một lớp bằng phản xạ tạo ra một thực thể mới bằng cách sử dụng hàm tạo công khai của nó (thường chỉ lấy mã định danh) và gọi một mảng Hành động <TEntity> trên đó, lưu ảnh chụp nhanh sau mỗi thao tác (với tên thông thường dựa trên chỉ mục của hành động và tên thực thể). Công cụ này được thực thi thủ công tại mỗi lần tái cấu trúc mã thực thể và các ảnh chụp nhanh được theo dõi bởi DCVS. Rõ ràng mỗi Hành động gọi một lệnh công khai của thực thể, nhưng điều này được thực hiện trong các thử nghiệm chạy theo cách này thực sự là thử nghiệm Đơn vị .
Giacomo Tesio

Tôi không hiểu điều đó thay đổi như thế nào. Nếu nó vẫn gọi các phương thức công khai trên sut (hệ thống đang thử nghiệm) thì nó không khác gì, chỉ cần gọi các phương thức đó trong thử nghiệm.
Piotr Perak

Sau khi ảnh chụp nhanh được tạo ra, chúng được lưu trữ trong các tệp. Mỗi thử nghiệm không phụ thuộc vào chuỗi các thao tác cần thiết để có được trạng thái bắt đầu của thực thể, mà từ chính trạng thái (được tải từ ảnh chụp nhanh). Phương thức được thử nghiệm sau đó được tách ra khỏi các thay đổi so với các phương thức khác.
Giacomo Tesio

Điều gì xảy ra khi ai đó thay đổi phương thức công khai được sử dụng để chuẩn bị trạng thái tuần tự hóa cho các thử nghiệm của bạn mà quên chạy công cụ để tạo lại đối tượng được tuần tự hóa? Các thử nghiệm vẫn có màu xanh ngay cả khi có lỗi trong mã. Tôi vẫn nói điều này không thay đổi bất cứ điều gì. Bạn vẫn chạy các phương thức công khai để thiết lập các đối tượng bạn kiểm tra. Nhưng bạn chạy chúng lâu trước khi chạy thử.
Piotr Perak

-7

Bạn nói

cố gắng áp dụng các thực hành tốt nhất bất cứ khi nào có thể

ORM mà chúng ta đang sử dụng để hydrat hóa các đối tượng sử dụng sự phản chiếu để nó có thể truy cập vào setters riêng

và tôi phải nghĩ rằng việc sử dụng sự phản chiếu để bỏ qua các điều khiển truy cập trên các lớp của bạn không phải là điều tôi mô tả là "thực tiễn tốt nhất". Nó cũng sẽ chậm kinh khủng.


Cá nhân, tôi sẽ loại bỏ khung kiểm tra đơn vị của bạn và đi với một cái gì đó trong lớp - có vẻ như bạn đang viết bài kiểm tra từ quan điểm kiểm tra toàn bộ lớp dù sao, điều đó là tốt. Trước đây, đối với một số thành phần phức tạp cần thử nghiệm, tôi đã nhúng mã xác nhận và mã thiết lập vào chính lớp đó (nó từng là một mẫu thiết kế chung để có phương thức test () trong mỗi lớp), vì vậy bạn tạo một máy khách điều đó chỉ đơn giản là khởi tạo một đối tượng và gọi phương thức thử nghiệm có thể tự thiết lập tùy thích mà không gây khó chịu như hack phản xạ.

Nếu bạn lo ngại về sự phình mã, chỉ cần bọc các phương thức thử nghiệm trong #ifdefs để làm cho chúng chỉ có sẵn trong mã gỡ lỗi (có thể là một cách thực hành tốt nhất)


4
-1: Loại bỏ khung kiểm tra của bạn và quay lại các phương thức kiểm tra bên trong lớp sẽ quay trở lại thời kỳ đen tối của kiểm thử đơn vị.
Robert Johnson

9
Không có -1 từ tôi, nhưng bao gồm cả mã kiểm tra trong sản xuất nói chung là một điều xấu (TM) .
Peter K.

OP còn làm gì nữa? Dính vào vít với setters tư nhân?! Nó giống như chọn chất độc mà bạn muốn uống. Đề nghị của tôi với OP là đưa thử nghiệm đơn vị vào mã gỡ lỗi, không sản xuất. Theo kinh nghiệm của tôi, việc đưa các bài kiểm tra đơn vị vào một dự án khác chỉ có nghĩa là dự án đó được liên kết chặt chẽ với bản gốc, vì vậy, từ một nhà phát triển PoV, có rất ít sự khác biệt.
gbjbaanb
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.