Mã thử nghiệm đơn vị với một phụ thuộc hệ thống tập tin


138

Tôi đang viết một thành phần, được cung cấp một tệp ZIP, cần phải:

  1. Giải nén tập tin.
  2. Tìm một dll cụ thể trong số các tập tin được giải nén.
  3. Tải dll đó thông qua sự phản chiếu và gọi một phương thức trên nó.

Tôi muốn đơn vị kiểm tra thành phần này.

Tôi muốn viết mã liên quan trực tiếp đến hệ thống tệp:

void DoIt()
{
   Zip.Unzip(theZipFile, "C:\\foo\\Unzipped");
   System.IO.File myDll = File.Open("C:\\foo\\Unzipped\\SuperSecret.bar");
   myDll.InvokeSomeSpecialMethod();
}

Nhưng mọi người thường nói, "Đừng viết các bài kiểm tra đơn vị dựa trên hệ thống tệp, cơ sở dữ liệu, mạng, v.v."

Nếu tôi viết nó theo cách thân thiện với bài kiểm tra đơn vị, tôi cho rằng nó sẽ giống như thế này:

void DoIt(IZipper zipper, IFileSystem fileSystem, IDllRunner runner)
{
   string path = zipper.Unzip(theZipFile);
   IFakeFile file = fileSystem.Open(path);
   runner.Run(file);
}

Yay! Bây giờ nó có thể kiểm tra được; Tôi có thể cung cấp thử nghiệm nhân đôi (giả) cho phương pháp DoIt. Nhưng với chi phí nào? Bây giờ tôi đã phải xác định 3 giao diện mới chỉ để làm cho thử nghiệm này. Và chính xác thì tôi đang thử nghiệm cái gì? Tôi đang kiểm tra rằng chức năng DoIt của tôi tương tác đúng với các phụ thuộc của nó. Nó không kiểm tra rằng tệp zip đã được giải nén đúng cách, v.v.

Nó không cảm thấy như tôi đang thử nghiệm chức năng nữa. Cảm giác như tôi chỉ đang thử nghiệm các tương tác trong lớp.

Câu hỏi của tôi là : cách thích hợp để kiểm tra đơn vị gì đó phụ thuộc vào hệ thống tệp?

chỉnh sửa Tôi đang sử dụng .NET, nhưng khái niệm này cũng có thể áp dụng Java hoặc mã gốc.


8
Mọi người nói không viết vào hệ thống tệp trong bài kiểm tra đơn vị vì nếu bạn muốn ghi vào hệ thống tệp, bạn không hiểu điều gì cấu thành bài kiểm tra đơn vị. Một thử nghiệm đơn vị thường tương tác với một đối tượng thực duy nhất (đơn vị được thử nghiệm) và tất cả các phụ thuộc khác được chế giễu và truyền vào. Lớp thử nghiệm sau đó bao gồm các phương thức thử nghiệm xác nhận các đường dẫn logic thông qua các phương thức của đối tượng và CHỈ các đường dẫn logic trong đơn vị đang thử nghiệm.
Christopher Perry

1
trong tình huống của bạn, phần duy nhất cần kiểm tra đơn vị sẽ là myDll.InvokeSomeSpecialMethod();nơi bạn sẽ kiểm tra xem nó có hoạt động chính xác trong cả tình huống thành công và thất bại không, vì vậy tôi sẽ không kiểm tra đơn vị DoItnhưng DllRunner.Runnói rằng việc lạm dụng một bài kiểm tra UNIT để kiểm tra lại rằng toàn bộ quá trình có hoạt động không một sự lạm dụng có thể chấp nhận được và vì nó sẽ là một thử nghiệm tích hợp giả mạo một bài kiểm tra đơn vị, các quy tắc kiểm tra đơn vị bình thường không cần phải được áp dụng một cách nghiêm ngặt
MikeT

Câu trả lời:


47

Thực sự không có gì sai với điều này, đó chỉ là câu hỏi liệu bạn gọi nó là kiểm tra đơn vị hay kiểm tra tích hợp. Bạn chỉ cần đảm bảo rằng nếu bạn tương tác với hệ thống tệp, không có tác dụng phụ ngoài ý muốn. Cụ thể, đảm bảo rằng bạn dọn dẹp sau khi bạn - xóa bất kỳ tệp tạm thời nào bạn đã tạo - và bạn không vô tình ghi đè lên một tệp hiện có có cùng tên tệp với tệp tạm thời bạn đang sử dụng. Luôn luôn sử dụng các đường dẫn tương đối và không phải là đường dẫn tuyệt đối.

Nó cũng là một ý tưởng tốt để chdir()vào một thư mục tạm thời trước khi chạy thử nghiệm của bạn và chdir()trở lại sau đó.


27
Tuy nhiên, +1 lưu ý rằng toàn chdir()quy trình để bạn có thể phá vỡ khả năng chạy song song các thử nghiệm của mình, nếu khung thử nghiệm của bạn hoặc phiên bản tương lai của nó hỗ trợ điều đó.

69

Yay! Bây giờ nó có thể kiểm tra được; Tôi có thể cung cấp thử nghiệm nhân đôi (giả) cho phương pháp DoIt. Nhưng với chi phí nào? Bây giờ tôi đã phải xác định 3 giao diện mới chỉ để làm cho thử nghiệm này. Và chính xác thì tôi đang thử nghiệm cái gì? Tôi đang kiểm tra rằng chức năng DoIt của tôi tương tác đúng với các phụ thuộc của nó. Nó không kiểm tra rằng tệp zip đã được giải nén đúng cách, v.v.

Bạn đã đánh móng tay ngay trên đầu của nó. Những gì bạn muốn kiểm tra là logic của phương pháp của bạn, không nhất thiết là liệu một tập tin thực sự có thể được giải quyết hay không. Bạn không cần phải kiểm tra (trong bài kiểm tra đơn vị này) xem một tệp có được giải nén chính xác hay không, phương pháp của bạn sẽ được chấp nhận. Các giao diện có giá trị bởi vì chúng cung cấp các khái niệm trừu tượng mà bạn có thể lập trình chống lại, thay vì dựa hoàn toàn hoặc rõ ràng dựa vào một triển khai cụ thể.


12
Các DoItchức năng có thể kiểm tra như đã nêu thậm chí không cần kiểm tra. Như người hỏi phải chỉ ra rằng không còn gì quan trọng để kiểm tra. Bây giờ là việc thực hiện IZipper, IFileSystemIDllRunnerrằng nhu cầu thử nghiệm, nhưng họ là những điều rất đã được chế giễu ra cho kỳ thi này!
Ian Goldby

56

Câu hỏi của bạn cho thấy một trong những phần khó nhất của thử nghiệm dành cho các nhà phát triển mới tham gia vào nó:

"Cái quái gì tôi kiểm tra?"

Ví dụ của bạn không thú vị lắm vì nó chỉ kết hợp một số lệnh gọi API với nhau, vì vậy nếu bạn viết một bài kiểm tra đơn vị cho nó, bạn sẽ chỉ khẳng định rằng các phương thức được gọi. Các thử nghiệm như thế này kết hợp chặt chẽ các chi tiết thực hiện của bạn với thử nghiệm. Điều này là xấu vì bây giờ bạn phải thay đổi thử nghiệm mỗi khi bạn thay đổi chi tiết triển khai phương thức của mình vì thay đổi chi tiết triển khai sẽ phá vỡ (các) thử nghiệm của bạn!

Có các bài kiểm tra xấu thực sự tồi tệ hơn là không có bài kiểm tra nào cả.

Trong ví dụ của bạn:

void DoIt(IZipper zipper, IFileSystem fileSystem, IDllRunner runner)
{
   string path = zipper.Unzip(theZipFile);
   IFakeFile file = fileSystem.Open(path);
   runner.Run(file);
}

Mặc dù bạn có thể vượt qua trong giả, không có logic trong phương pháp để kiểm tra. Nếu bạn đã thử một bài kiểm tra đơn vị cho việc này, nó có thể trông giống như thế này:

// Assuming that zipper, fileSystem, and runner are mocks
void testDoIt()
{
  // mock behavior of the mock objects
  when(zipper.Unzip(any(File.class)).thenReturn("some path");
  when(fileSystem.Open("some path")).thenReturn(mock(IFakeFile.class));

  // run the test
  someObject.DoIt(zipper, fileSystem, runner);

  // verify things were called
  verify(zipper).Unzip(any(File.class));
  verify(fileSystem).Open("some path"));
  verify(runner).Run(file);
}

Xin chúc mừng, về cơ bản, bạn đã dán các chi tiết triển khai DoIt()phương pháp của mình vào một bài kiểm tra. Hạnh phúc duy trì.

Khi bạn viết bài kiểm tra, bạn muốn kiểm tra CÁI GÌ chứ không phải CÁCH . Xem Kiểm tra hộp đen để biết thêm.

Các là tên của phương pháp của bạn (hoặc ít nhất là nó nên có). Các CÁCH là tất cả các chi tiết thực hiện ít mà sống bên trong phương pháp của bạn. Các bài kiểm tra tốt cho phép bạn trao đổi CÁCH mà không phá vỡ CÁI GÌ .

Nghĩ về nó theo cách này, hãy tự hỏi:

"Nếu tôi thay đổi chi tiết thực hiện của phương pháp này (mà không thay đổi hợp đồng công khai) thì nó có phá vỡ (các) thử nghiệm của tôi không?"

Nếu câu trả lời là có, bạn đang kiểm tra CÁCH chứ không phải CÁI GÌ .

Để trả lời câu hỏi cụ thể của bạn về mã kiểm tra với các phụ thuộc hệ thống tệp, giả sử bạn có điều gì đó thú vị hơn một chút với tệp và bạn muốn lưu nội dung được mã hóa Base64 của byte[]tệp vào tệp. Bạn có thể sử dụng các luồng cho việc này để kiểm tra xem mã của bạn có làm đúng hay không mà phải kiểm tra xem nó làm như thế nào . Một ví dụ có thể giống như thế này (trong Java):

interface StreamFactory {
    OutputStream outStream();
    InputStream inStream();
}

class Base64FileWriter {
    public void write(byte[] contents, StreamFactory streamFactory) {
        OutputStream outputStream = streamFactory.outStream();
        outputStream.write(Base64.encodeBase64(contents));
    }
}

@Test
public void save_shouldBase64EncodeContents() {
    OutputStream outputStream = new ByteArrayOutputStream();
    StreamFactory streamFactory = mock(StreamFactory.class);
    when(streamFactory.outStream()).thenReturn(outputStream);

    // Run the method under test
    Base64FileWriter fileWriter = new Base64FileWriter();
    fileWriter.write("Man".getBytes(), streamFactory);

    // Assert we saved the base64 encoded contents
    assertThat(outputStream.toString()).isEqualTo("TWFu");
}

Xét nghiệm này sử dụng một ByteArrayOutputStreamnhưng trong ứng dụng (sử dụng dependency injection) các StreamFactory thực (có lẽ gọi là FileStreamFactory) sẽ quay trở lại FileOutputStreamtừ outputStream()và sẽ viết thư cho một File.

Điều thú vị về writephương pháp ở đây là nó đã viết nội dung ra mã hóa Base64, vì vậy đó là những gì chúng tôi đã thử nghiệm. Đối với DoIt()phương pháp của bạn , điều này sẽ được kiểm tra phù hợp hơn với kiểm tra tích hợp .


1
Tôi không chắc chắn tôi đồng ý với tin nhắn của bạn ở đây. Bạn đang nói rằng không cần thiết phải kiểm tra loại phương pháp này? Vì vậy, về cơ bản bạn đang nói TDD là xấu? Như thể bạn làm TDD thì bạn không thể viết phương pháp này mà không viết bài kiểm tra trước. Hay bạn dựa vào linh cảm rằng phương pháp của bạn sẽ không yêu cầu kiểm tra? Lý do TẤT CẢ các khung kiểm tra đơn vị bao gồm tính năng "xác minh" là vì sử dụng nó là ổn. "Điều này thật tệ vì bây giờ bạn phải thay đổi thử nghiệm mỗi khi bạn thay đổi chi tiết triển khai phương pháp của mình" ... chào mừng bạn đến với thế giới thử nghiệm đơn vị.
Ronnie

2
Bạn có nghĩa vụ kiểm tra HỢP ĐỒNG của một phương pháp, không phải là triển khai. Nếu bạn phải thay đổi thử nghiệm của mình mỗi khi việc triển khai hợp đồng đó thay đổi thì bạn sẽ phải mất một thời gian khủng khiếp để duy trì cả cơ sở mã ứng dụng và cơ sở mã thử nghiệm.
Christopher Perry

@Ronnie mù quáng áp dụng thử nghiệm đơn vị là không hữu ích. Có những dự án có tính chất thay đổi rộng rãi, và thử nghiệm đơn vị không hiệu quả trong tất cả chúng. Ví dụ, tôi đang làm việc trong một dự án trong đó 95% mã là về các tác dụng phụ (lưu ý, bản chất nặng về tác dụng phụ này là theo yêu cầu , đó là sự phức tạp thiết yếu, không phải ngẫu nhiên , vì nó thu thập dữ liệu từ một loạt các nguồn trạng thái và trình bày nó với rất ít thao tác, do đó hầu như không có logic thuần túy nào). Kiểm thử đơn vị ở đây không hiệu quả, kiểm thử tích hợp là.
Vicky Chijwani

Các tác dụng phụ nên được đẩy ra các cạnh của hệ thống của bạn, chúng không nên được đan xen trong suốt các lớp. Ở các cạnh bạn kiểm tra các tác dụng phụ, đó là hành vi. Ở mọi nơi khác, bạn nên cố gắng có các chức năng thuần túy mà không có tác dụng phụ, dễ dàng kiểm tra và dễ dàng lý luận, tái sử dụng và sáng tác.
Christopher Perry

24

Tôi thận trọng để làm ô nhiễm mã của tôi với các loại và khái niệm chỉ tồn tại để tạo điều kiện cho thử nghiệm đơn vị. Chắc chắn, nếu nó làm cho thiết kế sạch hơn và tốt hơn thì tuyệt vời, nhưng tôi nghĩ đó thường không phải là trường hợp.

Tôi cho rằng điều này là các bài kiểm tra đơn vị của bạn sẽ làm nhiều nhất có thể mà có thể không được bảo hiểm 100%. Trong thực tế, nó có thể chỉ là 10%. Vấn đề là, bài kiểm tra đơn vị của bạn phải nhanh và không có phụ thuộc bên ngoài. Họ có thể kiểm tra các trường hợp như "phương thức này đưa ra một ArgumentNullException khi bạn chuyển thành null cho tham số này".

Sau đó, tôi sẽ thêm các thử nghiệm tích hợp (cũng tự động và có thể sử dụng cùng một khung thử nghiệm đơn vị) có thể có các phụ thuộc bên ngoài và thử nghiệm các kịch bản từ đầu đến cuối như các kịch bản này.

Khi đo độ bao phủ mã, tôi đo cả hai bài kiểm tra đơn vị và tích hợp.


5
Vâng, tôi nghe thấy bạn. Có một thế giới kỳ quái mà bạn đạt đến nơi bạn đã tách rời rất nhiều, tất cả những gì còn lại của bạn là những lời mời gọi phương thức trên các đối tượng trừu tượng. Thoáng lông. Khi bạn đạt đến điểm này, không có cảm giác như bạn đang thực sự thử nghiệm bất cứ điều gì thực sự. Bạn chỉ đang kiểm tra sự tương tác giữa các lớp.
Judah Gabriel Himango

6
Câu trả lời này là sai lầm. Kiểm tra đơn vị không giống như phủ sương, nó giống như đường. Nó được nướng vào bánh. Đó là một phần của việc viết mã của bạn ... một hoạt động thiết kế. Do đó, bạn không bao giờ "làm ô nhiễm" mã của mình bằng bất cứ điều gì có thể "tạo điều kiện cho việc kiểm tra" bởi vì việc kiểm tra là điều tạo điều kiện cho bạn viết mã của mình. 99% thời gian một bài kiểm tra khó viết vì nhà phát triển đã viết mã trước khi kiểm tra và cuối cùng đã viết mã không thể kiểm chứng
Christopher Perry

1
@Christopher: để mở rộng sự tương tự của bạn, tôi không muốn chiếc bánh của mình giống như một lát vani chỉ để tôi có thể sử dụng đường. Tất cả những gì tôi ủng hộ là chủ nghĩa thực dụng.
Kent Boogaart

1
@Christopher: tiểu sử của bạn nói lên tất cả: "Tôi là một người nhiệt tình TDD". Tôi, mặt khác, thực dụng. Tôi làm TDD ở nơi phù hợp và không phải ở nơi không phù hợp - không có gì trong câu trả lời của tôi cho thấy tôi không làm TDD, mặc dù bạn dường như nghĩ như vậy. Và cho dù đó là TDD hay không, tôi sẽ không giới thiệu số lượng lớn phức tạp nhằm mục đích tạo điều kiện cho thử nghiệm.
Kent Boogaart

3
@ChristopherPerry Bạn có thể giải thích cách giải quyết vấn đề ban đầu của OP sau đó theo cách TDD không? Tôi chạy vào đây mọi lúc; Tôi cần phải viết một hàm có mục đích duy nhất là thực hiện một hành động với sự phụ thuộc bên ngoài, như trong câu hỏi này. Vì vậy, ngay cả trong kịch bản viết thử nghiệm đầu tiên, thử nghiệm đó thậm chí sẽ là gì?
Dax Fohl

8

Không có gì sai khi nhấn hệ thống tập tin, chỉ coi đó là một bài kiểm tra tích hợp chứ không phải là một bài kiểm tra đơn vị. Tôi sẽ trao đổi đường dẫn được mã hóa cứng bằng một đường dẫn tương đối và tạo một thư mục con TestData để chứa các khóa cho các bài kiểm tra đơn vị.

Nếu các bài kiểm tra tích hợp của bạn mất quá nhiều thời gian để chạy thì hãy tách chúng ra để chúng không chạy thường xuyên như các bài kiểm tra đơn vị nhanh của bạn.

Tôi đồng ý, đôi khi tôi nghĩ thử nghiệm dựa trên tương tác có thể gây ra quá nhiều khớp nối và thường kết thúc không cung cấp đủ giá trị. Bạn thực sự muốn kiểm tra việc giải nén tệp ở đây không chỉ xác minh rằng bạn đang gọi đúng phương thức.


Làm thế nào họ thường chạy là ít quan tâm; chúng tôi sử dụng một máy chủ tích hợp liên tục tự động chạy chúng cho chúng tôi. Chúng tôi không thực sự quan tâm họ mất bao lâu. Nếu "chạy trong bao lâu" không phải là một mối quan tâm, thì có lý do nào để phân biệt giữa các bài kiểm tra đơn vị và tích hợp không?
Judah Gabriel Himango

4
Không hẳn vậy. Nhưng nếu các nhà phát triển muốn nhanh chóng chạy tất cả các bài kiểm tra đơn vị cục bộ thì thật dễ dàng để có một cách dễ dàng để làm điều đó.
JC.

6

Một cách sẽ là viết phương thức giải nén để lấy InputStreams. Sau đó, kiểm tra đơn vị có thể xây dựng một InputStream như vậy từ một mảng byte bằng cách sử dụng ByteArrayInputStream. Nội dung của mảng byte đó có thể là một hằng số trong mã kiểm tra đơn vị.


Ok, do đó cho phép tiêm luồng. Phụ thuộc tiêm / IOC. Làm thế nào về phần giải nén luồng vào các tệp, tải một dll trong số các tệp đó và gọi một phương thức trong dll đó?
Judah Gabriel Himango

3

Đây có vẻ là một thử nghiệm tích hợp nhiều hơn vì bạn phụ thuộc vào một chi tiết cụ thể (hệ thống tệp) có thể thay đổi, theo lý thuyết.

Tôi sẽ trừu tượng mã liên quan đến HĐH thành mô-đun của riêng nó (lớp, lắp ráp, jar, bất cứ thứ gì). Trong trường hợp của bạn, bạn muốn tải một DLL cụ thể nếu được tìm thấy, vì vậy hãy tạo giao diện IDllLoader và lớp DLLLoader. Ứng dụng của bạn có nhận được DLL từ DLLLoader bằng giao diện và kiểm tra xem .. bạn không chịu trách nhiệm về mã giải nén phải không?


2

Giả sử rằng "tương tác hệ thống tệp" được kiểm tra tốt trong chính khung công tác, tạo phương thức của bạn để làm việc với các luồng và kiểm tra điều này. Mở FileStream và chuyển nó sang phương thức có thể được bỏ qua các thử nghiệm của bạn, vì FileStream.Open được kiểm tra tốt bởi những người tạo khung.


Bạn và nsayer về cơ bản có cùng một đề xuất: làm cho mã của tôi hoạt động với các luồng. Làm thế nào về phần giải nén nội dung luồng vào các tệp dll, mở dll đó và gọi một hàm trong đó? Bạn sẽ làm gì ở đó?
Judah Gabriel Himango

3
@JudahHimango. Những phần đó có thể không nhất thiết phải kiểm tra được. Bạn không thể kiểm tra mọi thứ. Tóm tắt các thành phần không thể kiểm tra thành các khối chức năng riêng của chúng và cho rằng chúng sẽ hoạt động. Khi bạn gặp một lỗi với cách thức hoạt động của khối này, sau đó nghĩ ra một thử nghiệm cho nó và voila. Kiểm tra đơn vị KHÔNG có nghĩa là bạn phải kiểm tra tất cả mọi thứ. Bảo hiểm mã 100% là không thực tế trong một số tình huống.
Zoran Pavlovic

1

Bạn không nên kiểm tra tương tác lớp và gọi hàm. thay vào đó bạn nên xem xét thử nghiệm tích hợp. Kiểm tra kết quả yêu cầu và không hoạt động tải tập tin.


1

Đối với thử nghiệm đơn vị, tôi khuyên bạn nên bao gồm tệp thử nghiệm trong dự án của mình (tệp EAR hoặc tương đương) sau đó sử dụng đường dẫn tương đối trong các thử nghiệm đơn vị, ví dụ: "../testdata/testfile".

Miễn là dự án của bạn được xuất / nhập chính xác hơn kiểm tra đơn vị của bạn sẽ hoạt động.


0

Như những người khác đã nói, lần đầu tiên là tốt như một bài kiểm tra tích hợp. Các thử nghiệm thứ hai chỉ những gì chức năng được cho là thực sự làm, đó là tất cả các thử nghiệm đơn vị nên làm.

Như được hiển thị, ví dụ thứ hai có vẻ hơi vô nghĩa, nhưng nó cho bạn cơ hội để kiểm tra cách hàm phản ứng với các lỗi trong bất kỳ bước nào. Bạn không có bất kỳ lỗi kiểm tra nào trong ví dụ, nhưng trong hệ thống thực bạn có thể có, và việc tiêm phụ thuộc sẽ cho phép bạn kiểm tra tất cả các phản hồi đối với bất kỳ lỗi nào. Sau đó, chi phí sẽ có giá trị nó.

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.