Chiến lược tốt nhất cho các ứng dụng dựa trên cơ sở dữ liệu thử nghiệm đơn vị là gì?


346

Tôi làm việc với rất nhiều ứng dụng web được điều khiển bởi các cơ sở dữ liệu có độ phức tạp khác nhau trên phần phụ trợ. Thông thường, có một lớp ORM tách biệt với logic trình bày và kinh doanh. Điều này làm cho việc kiểm thử đơn vị logic kinh doanh khá đơn giản; mọi thứ có thể được thực hiện trong các mô-đun riêng biệt và bất kỳ dữ liệu nào cần thiết cho thử nghiệm có thể được làm giả thông qua việc chế nhạo đối tượng.

Nhưng việc kiểm tra ORM và bản thân cơ sở dữ liệu luôn gặp nhiều vấn đề và thỏa hiệp.

Trong những năm qua, tôi đã thử một vài chiến lược, không có chiến lược nào hoàn toàn làm tôi hài lòng.

  • Tải một cơ sở dữ liệu thử nghiệm với dữ liệu đã biết. Chạy thử nghiệm đối với ORM và xác nhận rằng dữ liệu phù hợp sẽ quay trở lại. Nhược điểm ở đây là DB thử nghiệm của bạn phải theo kịp mọi thay đổi lược đồ trong cơ sở dữ liệu ứng dụng và có thể không đồng bộ. Nó cũng dựa trên dữ liệu nhân tạo và có thể không để lộ các lỗi xảy ra do đầu vào của người dùng ngu ngốc. Cuối cùng, nếu cơ sở dữ liệu kiểm tra nhỏ, nó sẽ không tiết lộ sự thiếu hiệu quả như một chỉ mục bị thiếu. (OK, cái cuối cùng không thực sự nên sử dụng thử nghiệm đơn vị nào, nhưng nó không gây hại.)

  • Tải một bản sao của cơ sở dữ liệu sản xuất và kiểm tra chống lại điều đó. Vấn đề ở đây là bạn có thể không biết DB trong sản xuất tại bất kỳ thời điểm nào; các bài kiểm tra của bạn có thể cần phải được viết lại nếu dữ liệu thay đổi theo thời gian.

Một số người đã chỉ ra rằng cả hai chiến lược này đều dựa trên dữ liệu cụ thể và thử nghiệm đơn vị chỉ nên kiểm tra chức năng. Cuối cùng, tôi đã thấy đề xuất:

  • Sử dụng máy chủ cơ sở dữ liệu giả và chỉ kiểm tra xem ORM đang gửi các truy vấn chính xác để đáp ứng với một cuộc gọi phương thức đã cho.

Những chiến lược nào bạn đã sử dụng để thử nghiệm các ứng dụng dựa trên cơ sở dữ liệu, nếu có? Điều gì đã làm việc tốt nhất cho bạn?


Tôi nghĩ bạn vẫn nên có các chỉ mục cơ sở dữ liệu trong môi trường thử nghiệm cho các trường hợp như chỉ mục duy nhất.
dtc

Tôi không quan tâm đến câu hỏi này ở đây nhưng nếu chúng ta tuân theo các quy tắc, câu hỏi này không dành cho stackoverflow mà là dành cho trang web softwareengineering.stackexchange .
ITExpert

Câu trả lời:


155

Tôi thực sự đã sử dụng cách tiếp cận đầu tiên của bạn với khá nhiều thành công, nhưng theo một cách hơi khác mà tôi nghĩ sẽ giải quyết được một số vấn đề của bạn:

  1. Giữ toàn bộ lược đồ và tập lệnh để tạo nó trong kiểm soát nguồn để bất kỳ ai cũng có thể tạo lược đồ cơ sở dữ liệu hiện tại sau khi kiểm tra. Ngoài ra, giữ dữ liệu mẫu trong các tệp dữ liệu được tải bởi một phần của quy trình xây dựng. Khi bạn phát hiện ra dữ liệu gây ra lỗi, hãy thêm nó vào dữ liệu mẫu của bạn để kiểm tra xem lỗi đó có xuất hiện lại không.

  2. Sử dụng máy chủ tích hợp liên tục để xây dựng lược đồ cơ sở dữ liệu, tải dữ liệu mẫu và chạy thử nghiệm. Đây là cách chúng tôi giữ cho cơ sở dữ liệu thử nghiệm của chúng tôi đồng bộ hóa (xây dựng lại nó mỗi khi chạy thử). Mặc dù điều này đòi hỏi máy chủ CI có quyền truy cập và quyền sở hữu cá thể cơ sở dữ liệu chuyên dụng của riêng nó, tôi nói rằng việc xây dựng lược đồ db của chúng tôi 3 lần một ngày đã giúp tìm ra các lỗi có thể không được tìm thấy ngay trước khi giao hàng (nếu không muộn hơn ). Tôi không thể nói rằng tôi xây dựng lại lược đồ trước mỗi lần xác nhận. Có ai không? Với phương pháp này, bạn sẽ không phải (cũng có thể chúng ta nên, nhưng nó không phải là vấn đề lớn nếu ai đó quên).

  3. Đối với nhóm của tôi, đầu vào của người dùng được thực hiện ở cấp ứng dụng (không phải db) vì vậy điều này được kiểm tra thông qua các thử nghiệm đơn vị tiêu chuẩn.

Đang tải Bản sao cơ sở dữ liệu sản xuất:
Đây là cách tiếp cận được sử dụng trong công việc cuối cùng của tôi. Đó là một nguyên nhân đau đớn lớn của một vài vấn đề:

  1. Bản sao sẽ bị lỗi thời từ phiên bản sản xuất
  2. Các thay đổi sẽ được thực hiện cho lược đồ của bản sao và sẽ không được truyền bá tới các hệ thống sản xuất. Tại thời điểm này, chúng tôi sẽ có các lược đồ phân kỳ. Không vui.

Mocking Database Server:
Chúng tôi cũng làm điều này trong công việc hiện tại của tôi. Sau mỗi lần xác nhận, chúng tôi thực hiện các kiểm tra đơn vị đối với mã ứng dụng có các trình truy cập db giả định được tiêm. Sau đó ba lần một ngày chúng tôi thực hiện xây dựng db đầy đủ được mô tả ở trên. Tôi chắc chắn đề nghị cả hai cách tiếp cận.


37
Tải một bản sao cơ sở dữ liệu sản xuất cũng có ý nghĩa bảo mật và quyền riêng tư. Một khi nó trở nên lớn, lấy một bản sao của nó và đưa nó vào môi trường dev của bạn có thể là một vấn đề lớn.
Thế chiến.

Thành thật mà nói, đây là một nỗi đau rất lớn. Tôi mới thử nghiệm và tôi cũng đã viết một orm tôi muốn thử nghiệm. Tôi đã sử dụng phương pháp đầu tiên của bạn, nhưng đọc rằng nó không tạo ra đơn vị thử nghiệm. Tôi sử dụng chức năng công cụ db cụ thể và do đó, việc chế tạo DAO sẽ khó khăn. Tôi nghĩ rằng tôi chỉ sử dụng phương pháp hiện tại của tôi vì nó hoạt động và những người khác sử dụng nó. Kiểm tra tự động rock btw. Cảm ơn.
frostymarvelous

2
Tôi quản lý hai dự án lớn khác nhau, trong một trong số đó, cách tiếp cận này là hoàn hảo, nhưng chúng tôi đã gặp nhiều rắc rối khi cố gắng thực hiện điều này trong dự án khác. Vì vậy, tôi nghĩ rằng tùy thuộc vào mức độ có thể dễ dàng tạo lại lược đồ mỗi lần thực hiện các thử nghiệm, tôi hiện đang tìm kiếm một giải pháp mới cho vấn đề cuối cùng này.
Băng qua

2
Trong trường hợp này, chắc chắn nó đáng để sử dụng một công cụ phiên bản cơ sở dữ liệu như Roundhouse - thứ có thể chạy di chuyển. Điều này có thể được chạy trên bất kỳ trường hợp DB nào và phải đảm bảo rằng các lược đồ được cập nhật. Ngoài ra, khi các tập lệnh di chuyển được viết, dữ liệu kiểm tra cũng nên được viết - giữ cho việc di chuyển và dữ liệu được đồng bộ hóa.
jedd.ahyoung

sử dụng tốt hơn việc vá khỉ và chế giễu và tránh các thao tác viết
Nickpick

56

Tôi luôn chạy thử nghiệm đối với DB trong bộ nhớ (HSQLDB hoặc Derby) vì những lý do sau:

  • Nó khiến bạn nghĩ nên giữ dữ liệu nào trong DB thử nghiệm của mình và tại sao. Chỉ cần đưa DB sản xuất của bạn vào một hệ thống thử nghiệm có nghĩa là "Tôi không biết tôi đang làm gì hoặc tại sao và nếu có gì đó bị hỏng, đó không phải là tôi !!" ;)
  • Nó đảm bảo cơ sở dữ liệu có thể được tạo lại với một chút nỗ lực ở một nơi mới (ví dụ khi chúng ta cần sao chép một lỗi từ sản xuất)
  • Nó giúp rất nhiều với chất lượng của các tệp DDL.

DB trong bộ nhớ được tải với dữ liệu mới sau khi các thử nghiệm bắt đầu và sau hầu hết các thử nghiệm, tôi gọi ROLLBACK để giữ ổn định. LUÔN LUÔN giữ dữ liệu trong DB thử nghiệm ổn định! Nếu dữ liệu thay đổi liên tục, bạn không thể kiểm tra.

Dữ liệu được tải từ SQL, DB mẫu hoặc kết xuất / sao lưu. Tôi thích các bãi chứa nếu chúng ở định dạng có thể đọc được vì tôi có thể đặt chúng vào VCS. Nếu điều đó không hiệu quả, tôi sử dụng tệp CSV hoặc XML. Nếu tôi phải tải một lượng lớn dữ liệu ... tôi sẽ không. Bạn không bao giờ phải tải một lượng dữ liệu khổng lồ :) Không dành cho các bài kiểm tra đơn vị. Kiểm tra hiệu suất là một vấn đề khác và áp dụng các quy tắc khác nhau.


1
Có phải tốc độ là lý do duy nhất để sử dụng (cụ thể) một DB trong bộ nhớ?
rinogo

2
Tôi đoán một lợi thế khác có thể là bản chất "vứt đi" của nó - không cần phải tự dọn dẹp; Chỉ cần giết DB trong bộ nhớ. (Nhưng có nhiều cách khác để thực hiện điều này, chẳng hạn như phương pháp ROLLBACK mà bạn đã đề cập)
rinogo

1
Ưu điểm là mỗi thử nghiệm có thể chọn chiến lược riêng của mình. Chúng tôi có các bài kiểm tra thực hiện công việc trong các luồng con, điều đó có nghĩa là Spring sẽ luôn cam kết dữ liệu.
Aaron Digulla

@Aaron: chúng tôi cũng đang theo chiến lược này. Tôi muốn biết chiến lược của bạn là gì để khẳng định rằng mô hình trong bộ nhớ có cấu trúc giống với db thực?
Guillaume

1
@Guillaume: Tôi đang tạo tất cả các cơ sở dữ liệu từ cùng một tệp SQL. H2 là tuyệt vời cho điều này vì nó hỗ trợ hầu hết các đặc điểm riêng của SQL trong các cơ sở dữ liệu chính. Nếu điều đó không hoạt động, thì tôi sử dụng bộ lọc lấy SQL gốc và chuyển đổi nó thành SQL cho cơ sở dữ liệu trong bộ nhớ.
Aaron Digulla

14

Tôi đã hỏi câu hỏi này trong một thời gian dài, nhưng tôi nghĩ không có viên đạn bạc nào cho điều đó.

Những gì tôi hiện đang làm là chế nhạo các đối tượng DAO và giữ một bộ nhớ đại diện cho một bộ sưu tập tốt các đối tượng đại diện cho các trường hợp dữ liệu thú vị có thể sống trên cơ sở dữ liệu.

Vấn đề chính tôi thấy với cách tiếp cận đó là bạn chỉ bao gồm mã tương tác với lớp DAO của bạn, nhưng không bao giờ kiểm tra chính DAO và theo kinh nghiệm của tôi, tôi cũng thấy rằng có rất nhiều lỗi xảy ra trên lớp đó. Tôi cũng giữ một vài thử nghiệm đơn vị chạy với cơ sở dữ liệu (vì mục đích sử dụng TDD hoặc thử nghiệm nhanh cục bộ), nhưng các thử nghiệm đó không bao giờ chạy trên máy chủ tích hợp liên tục của tôi, vì chúng tôi không giữ cơ sở dữ liệu cho mục đích đó và tôi nghĩ rằng các bài kiểm tra chạy trên máy chủ CI nên được khép kín.

Một cách tiếp cận khác tôi thấy rất thú vị, nhưng không phải lúc nào cũng có giá trị vì tốn ít thời gian, là tạo cùng một lược đồ bạn sử dụng để sản xuất trên cơ sở dữ liệu nhúng chỉ chạy trong thử nghiệm đơn vị.

Mặc dù không có câu hỏi nào về cách tiếp cận này cải thiện phạm vi bảo hiểm của bạn, nhưng có một vài nhược điểm, vì bạn phải càng gần càng tốt với ANSI SQL để làm cho nó hoạt động cả với DBMS hiện tại của bạn và thay thế nhúng.

Bất kể điều gì bạn nghĩ là phù hợp hơn với mã của bạn, có một vài dự án ngoài kia có thể làm cho nó dễ dàng hơn, như DbUnit .


13

Thậm chí nếu có các công cụ cho phép bạn để thử cơ sở dữ liệu của bạn trong một cách này hay cách khác (ví dụ jOOQ 's MockConnection, có thể được nhìn thấy trong câu trả lời này - từ chối trách nhiệm, tôi làm việc cho nhà cung cấp jOOQ), tôi sẽ tư vấn cho không để thử cơ sở dữ liệu lớn hơn với phức tạp truy vấn.

Ngay cả khi bạn chỉ muốn kiểm tra tích hợp ORM của mình, hãy coi chừng ORM đưa ra một loạt các truy vấn rất phức tạp vào cơ sở dữ liệu của bạn, có thể khác nhau trong

  • cú pháp
  • phức tạp
  • đặt hàng (!)

Việc chế tạo tất cả những thứ đó để tạo ra dữ liệu giả hợp lý là khá khó, trừ khi bạn thực sự xây dựng một cơ sở dữ liệu nhỏ bên trong giả của mình, điều này diễn giải các câu lệnh SQL được truyền. Như đã nói, hãy sử dụng cơ sở dữ liệu kiểm thử tích hợp nổi tiếng mà bạn có thể dễ dàng đặt lại với dữ liệu nổi tiếng, dựa vào đó bạn có thể chạy các kiểm tra tích hợp của mình.


5

Tôi sử dụng cái đầu tiên (chạy mã dựa trên cơ sở dữ liệu thử nghiệm). Vấn đề thực sự duy nhất tôi thấy bạn nêu ra với cách tiếp cận này là khả năng các lược đồ không đồng bộ, mà tôi xử lý bằng cách giữ số phiên bản trong cơ sở dữ liệu của mình và thực hiện tất cả các thay đổi lược đồ thông qua tập lệnh áp dụng các thay đổi cho từng phiên bản.

Tôi cũng thực hiện tất cả các thay đổi (bao gồm cả lược đồ cơ sở dữ liệu) trước môi trường thử nghiệm của mình, vì vậy nó kết thúc theo cách khác: Sau khi tất cả các thử nghiệm vượt qua, hãy áp dụng các cập nhật lược đồ cho máy chủ sản xuất. Tôi cũng giữ một cặp thử nghiệm riêng so với cơ sở dữ liệu ứng dụng trên hệ thống phát triển của mình để tôi có thể xác minh rằng bản nâng cấp db hoạt động đúng trước khi chạm vào (các) hộp sản xuất thực.


3

Tôi đang sử dụng cách tiếp cận đầu tiên nhưng hơi khác một chút cho phép giải quyết các vấn đề bạn đã đề cập.

Mọi thứ cần thiết để chạy thử nghiệm cho DAO đều nằm trong kiểm soát nguồn. Nó bao gồm lược đồ và tập lệnh để tạo DB (docker rất tốt cho việc này). Nếu DB nhúng có thể được sử dụng - tôi sử dụng nó cho tốc độ.

Sự khác biệt quan trọng với các cách tiếp cận được mô tả khác là dữ liệu được yêu cầu để kiểm tra không được tải từ các tập lệnh SQL hoặc tệp XML. Mọi thứ (ngoại trừ một số dữ liệu từ điển có hiệu quả không đổi) được tạo bởi ứng dụng bằng các hàm / lớp tiện ích.

Mục đích chính là làm cho dữ liệu được sử dụng bởi thử nghiệm

  1. rất gần với bài kiểm tra
  2. rõ ràng (sử dụng các tệp SQL cho dữ liệu làm cho rất khó để xem phần dữ liệu nào được sử dụng bởi thử nghiệm gì)
  3. kiểm tra cô lập từ những thay đổi không liên quan.

Về cơ bản, điều đó có nghĩa là các tiện ích này cho phép khai báo chỉ định những thứ cần thiết cho thử nghiệm trong chính thử nghiệm và bỏ qua những thứ không liên quan.

Để đưa ra một số ý tưởng về ý nghĩa của nó trong thực tế, hãy xem xét thử nghiệm cho một số DAO hoạt động với Comments đến Posts được viết bởi Authors. Để kiểm tra các hoạt động CRUD cho DAO như vậy, một số dữ liệu phải được tạo trong DB. Bài kiểm tra sẽ như sau:

@Test
public void savedCommentCanBeRead() {
    // Builder is needed to declaratively specify the entity with all attributes relevant
    // for this specific test
    // Missing attributes are generated with reasonable values
    // factory's responsibility is to create entity (and all entities required by it
    //  in our example Author) in the DB
    Post post = factory.create(PostBuilder.post());

    Comment comment = CommentBuilder.comment().forPost(post).build();

    sut.save(comment);

    Comment savedComment = sut.get(comment.getId());

    // this checks fields that are directly stored
    assertThat(saveComment, fieldwiseEqualTo(comment));
    // if there are some fields that are generated during save check them separately
    assertThat(saveComment.getGeneratedField(), equalTo(expectedValue));        
}

Điều này có một số lợi thế so với các tập lệnh SQL hoặc tệp XML với dữ liệu thử nghiệm:

  1. Việc duy trì mã dễ dàng hơn nhiều (ví dụ: thêm một cột bắt buộc trong một số thực thể được tham chiếu trong nhiều thử nghiệm, như Tác giả, không yêu cầu thay đổi nhiều tệp / bản ghi mà chỉ cần thay đổi trong trình tạo và / hoặc nhà máy)
  2. Dữ liệu được yêu cầu bởi thử nghiệm cụ thể được mô tả trong chính thử nghiệm chứ không phải trong một số tệp khác. Sự gần gũi này rất quan trọng đối với tính dễ hiểu của bài kiểm tra.

Quay lại so với cam kết

Tôi thấy thuận tiện hơn khi các bài kiểm tra cam kết khi chúng được thực thi. Thứ nhất, một số hiệu ứng (ví dụ DEFERRED CONSTRAINTS) không thể được kiểm tra nếu cam kết không bao giờ xảy ra. Thứ hai, khi kiểm tra thất bại, dữ liệu có thể được kiểm tra trong DB vì nó không được hoàn nguyên bởi rollback.

Bởi vì điều này có một nhược điểm là thử nghiệm có thể tạo ra dữ liệu bị hỏng và điều này sẽ dẫn đến những thất bại trong các thử nghiệm khác. Để đối phó với điều này, tôi cố gắng cô lập các bài kiểm tra. Trong ví dụ trên, mọi thử nghiệm có thể tạo mới Authorvà tất cả các thực thể khác được tạo liên quan đến nó nên va chạm rất hiếm. Để đối phó với các bất biến còn lại có khả năng bị phá vỡ nhưng không thể biểu thị bằng ràng buộc mức DB, tôi sử dụng một số kiểm tra theo chương trình cho các điều kiện sai lầm có thể được chạy sau mỗi thử nghiệm (và chúng được chạy trong CI nhưng thường bị tắt cục bộ để thực hiện lý do).


Nếu bạn chọn cơ sở dữ liệu bằng cách sử dụng các thực thể và orm thay vì các tập lệnh sql, nó cũng có lợi thế là trình biên dịch sẽ buộc bạn sửa mã hạt giống nếu bạn thay đổi mô hình của mình. Tất nhiên chỉ liên quan nếu bạn sử dụng ngôn ngữ gõ tĩnh.
daramasala

Vì vậy, để làm rõ: bạn đang sử dụng các chức năng / lớp tiện ích trong ứng dụng của bạn, hay chỉ để kiểm tra?
Ella

@Ella các chức năng tiện ích này thường không cần thiết ngoài mã kiểm tra. Hãy suy nghĩ ví dụ về PostBuilder.post(). Nó tạo ra một số giá trị cho tất cả các thuộc tính bắt buộc của bài viết. Điều này là không cần thiết trong mã sản xuất.
La Mã Konoval

2

Đối với dự án dựa trên JDBC (trực tiếp hoặc gián tiếp, ví dụ JPA, EJB, ...), bạn có thể mô phỏng không phải toàn bộ cơ sở dữ liệu (trong trường hợp đó sẽ tốt hơn khi sử dụng db thử nghiệm trên RDBMS thực), nhưng chỉ mô phỏng ở cấp JDBC .

Ưu điểm là sự trừu tượng đi kèm theo cách đó, vì dữ liệu JDBC (tập kết quả, số lần cập nhật, cảnh báo, ...) giống nhau cho dù là phụ trợ: db prod của bạn, db thử nghiệm hoặc chỉ một số dữ liệu mô phỏng được cung cấp cho mỗi thử nghiệm trường hợp

Với kết nối JDBC được mô phỏng cho từng trường hợp, không cần phải quản lý db kiểm tra (dọn dẹp, chỉ có một thử nghiệm tại thời điểm, tải lại đồ đạc, ...). Mọi kết nối mockup đều bị cô lập và không cần phải dọn dẹp. Chỉ các đồ đạc yêu cầu tối thiểu được cung cấp trong mỗi trường hợp thử nghiệm để mô phỏng trao đổi JDBC, giúp tránh sự phức tạp trong việc quản lý toàn bộ db thử nghiệm.

Acolyte là khuôn khổ của tôi bao gồm trình điều khiển JDBC và tiện ích cho loại mockup này: http://acolyte.eu.org .

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.