Sử dụng Mockito để kiểm tra các lớp trừu tượng


213

Tôi muốn kiểm tra một lớp trừu tượng. Chắc chắn, tôi có thể tự viết một bản giả kế thừa từ lớp.

Tôi có thể làm điều này bằng cách sử dụng khung mô phỏng (tôi đang sử dụng Mockito) thay vì tự chế tạo bản giả của mình không? Làm sao?


2
Kể từ Mockito 1.10.12 , Mockito hỗ trợ trực tiếp các lớp trừu tượng / chế nhạo:SomeAbstract spy = spy(SomeAbstract.class);
pesche

6
Kể từ Mockito 2.7,14, bạn cũng có thể giả định lớp học trừu tượng yêu cầu đối số của nhà xây dựng thông quamock(MyAbstractClass.class, withSettings().useConstructor(arg1, arg2).defaultAnswer(CALLS_REAL_METHODS))
Gediminas Rimsa

Câu trả lời:


315

Gợi ý sau đây cho phép bạn kiểm tra các lớp trừu tượng mà không tạo lớp con "thực" - Mock lớp con.

sử dụng Mockito.mock(My.class, Mockito.CALLS_REAL_METHODS), sau đó mô phỏng bất kỳ phương thức trừu tượng nào được gọi.

Thí dụ:

public abstract class My {
  public Result methodUnderTest() { ... }
  protected abstract void methodIDontCareAbout();
}

public class MyTest {
    @Test
    public void shouldFailOnNullIdentifiers() {
        My my = Mockito.mock(My.class, Mockito.CALLS_REAL_METHODS);
        Assert.assertSomething(my.methodUnderTest());
    }
}

Lưu ý: Cái hay của giải pháp này là bạn không phải thực hiện các phương thức trừu tượng, miễn là chúng không bao giờ được gọi.

Theo ý kiến ​​trung thực của tôi, điều này gọn gàng hơn so với sử dụng một gián điệp, vì một gián điệp yêu cầu một thể hiện, có nghĩa là bạn phải tạo một lớp con có thể thực hiện được của lớp trừu tượng của bạn.


14
Như đã lưu ý dưới đây, điều này không hoạt động khi lớp trừu tượng gọi các phương thức trừu tượng để được kiểm tra, điều này thường xảy ra.
Richard Nichols

11
Điều này thực sự hoạt động khi lớp trừu tượng gọi các phương thức trừu tượng. Chỉ cần sử dụng cú pháp doReturn hoặc doNellow thay vì Mockito.when để ngắt các phương thức trừu tượng và nếu bạn thực hiện bất kỳ lệnh gọi cụ thể nào, hãy đảm bảo bỏ qua các cuộc gọi trừu tượng trước.
Gonen I

2
Làm thế nào tôi có thể tiêm các phụ thuộc vào loại đối tượng này (lớp trừu tượng giả định gọi các phương thức thực)?
Samuel

2
Điều này hành xử theo những cách bất ngờ nếu lớp trong câu hỏi có bộ khởi tạo cá thể. Mockito bỏ qua các trình khởi tạo cho các giả, có nghĩa là các biến thể hiện được khởi tạo nội tuyến sẽ bị vô hiệu hóa, điều này có thể gây ra NPE.
Digitalbath

1
Điều gì xảy ra nếu hàm tạo lớp trừu tượng lấy một hoặc nhiều tham số?
SD

68

Nếu bạn chỉ cần thử nghiệm một số phương pháp cụ thể mà không cần chạm vào bất kỳ tóm tắt nào, bạn có thể sử dụng CALLS_REAL_METHODS(xem câu trả lời của Morten ), nhưng nếu phương pháp cụ thể trong thử nghiệm gọi một số tóm tắt hoặc phương thức giao diện chưa được thực hiện, thì điều này sẽ không hiệu quả - Mockito sẽ phàn nàn "Không thể gọi phương thức thực trên giao diện java."

(Vâng, đó là một thiết kế tệ hại, nhưng một số khung, ví dụ Tapestry 4, loại lực lượng này đối với bạn.)

Cách giải quyết là đảo ngược cách tiếp cận này - sử dụng hành vi giả thông thường (nghĩa là mọi thứ bị chế giễu / sơ khai) và sử dụng doCallRealMethod()để gọi một cách rõ ràng phương pháp cụ thể đang được thử nghiệm. Ví dụ

public abstract class MyClass {
    @SomeDependencyInjectionOrSomething
    public abstract MyDependency getDependency();

    public void myMethod() {
        MyDependency dep = getDependency();
        dep.doSomething();
    }
}

public class MyClassTest {
    @Test
    public void myMethodDoesSomethingWithDependency() {
        MyDependency theDependency = mock(MyDependency.class);

        MyClass myInstance = mock(MyClass.class);

        // can't do this with CALLS_REAL_METHODS
        when(myInstance.getDependency()).thenReturn(theDependency);

        doCallRealMethod().when(myInstance).myMethod();
        myInstance.myMethod();

        verify(theDependency, times(1)).doSomething();
    }
}

Cập nhật để thêm:

Đối với các phương thức không trống, bạn sẽ cần sử dụng thenCallRealMethod()thay thế, ví dụ:

when(myInstance.myNonVoidMethod(someArgument)).thenCallRealMethod();

Nếu không, Mockito sẽ phàn nàn "Phát hiện chưa hoàn thành."


9
Điều này sẽ hoạt động trong một số trường hợp, tuy nhiên Mockito không gọi hàm tạo của lớp trừu tượng cơ bản bằng phương thức này. Điều này có thể khiến "phương thức thực" bị lỗi do một kịch bản không mong muốn được tạo. Do đó, phương pháp này cũng sẽ không hoạt động trong mọi trường hợp.
Richard Nichols

3
Đúng, bạn không thể tin vào trạng thái của đối tượng, chỉ có mã trong phương thức được gọi.
David Moles

Oh vậy các phương thức đối tượng được tách ra khỏi trạng thái, thật tuyệt.
haelix

17

Bạn có thể đạt được điều này bằng cách sử dụng một gián điệp (mặc dù sử dụng phiên bản mới nhất của Mockito 1.8+).

public abstract class MyAbstract {
  public String concrete() {
    return abstractMethod();
  }
  public abstract String abstractMethod();
}

public class MyAbstractImpl extends MyAbstract {
  public String abstractMethod() {
    return null;
  }
}

// your test code below

MyAbstractImpl abstractImpl = spy(new MyAbstractImpl());
doReturn("Blah").when(abstractImpl).abstractMethod();
assertTrue("Blah".equals(abstractImpl.concrete()));

14

Các khung mô phỏng được thiết kế để giúp dễ dàng giả định các phụ thuộc của lớp bạn đang kiểm tra. Khi bạn sử dụng khung mô phỏng để giả định một lớp, hầu hết các khung sẽ tự động tạo một lớp con và thay thế việc thực hiện phương thức bằng mã để phát hiện khi một phương thức được gọi và trả về giá trị giả.

Khi kiểm tra một lớp trừu tượng, bạn muốn thực thi các phương thức không trừu tượng của Chủ đề đang thử nghiệm (SUT), vì vậy một khung mô phỏng không phải là điều bạn muốn.

Một phần của sự nhầm lẫn là câu trả lời cho câu hỏi mà bạn liên kết đã nói để làm thủ công một bản giả kéo dài từ lớp trừu tượng của bạn. Tôi sẽ không gọi một lớp như vậy là một giả. Mock là một lớp được sử dụng để thay thế cho một phụ thuộc, được lập trình với các kỳ vọng và có thể được truy vấn để xem liệu những mong muốn đó có được đáp ứng hay không.

Thay vào đó, tôi đề nghị xác định một lớp con không trừu tượng của lớp trừu tượng trong bài kiểm tra của bạn. Nếu điều đó dẫn đến quá nhiều mã, thì đó có thể là một dấu hiệu cho thấy lớp của bạn khó mở rộng.

Một giải pháp thay thế sẽ là làm cho trường hợp thử nghiệm của bạn trở nên trừu tượng, với một phương thức trừu tượng để tạo SUT (nói cách khác, trường hợp thử nghiệm sẽ sử dụng mẫu thiết kế Phương thức mẫu).


8

Hãy thử sử dụng một câu trả lời tùy chỉnh.

Ví dụ:

import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;

public class CustomAnswer implements Answer<Object> {

    public Object answer(InvocationOnMock invocation) throws Throwable {

        Answer<Object> answer = null;

        if (isAbstract(invocation.getMethod().getModifiers())) {

            answer = Mockito.RETURNS_DEFAULTS;

        } else {

            answer = Mockito.CALLS_REAL_METHODS;
        }

        return answer.answer(invocation);
    }
}

Nó sẽ trả về giả cho các phương thức trừu tượng và sẽ gọi phương thức thực cho các phương thức cụ thể.


5

Điều thực sự khiến tôi cảm thấy tồi tệ khi chế nhạo các lớp trừu tượng là thực tế, rằng cả hàm tạo mặc định YourAbTHERClass () không được gọi (thiếu super () trong giả) và dường như không có cách nào trong Mockito để khởi tạo mặc định các thuộc tính giả định (ví dụ: Thuộc tính danh sách với ArrayList trống hoặc LinkedList).

Lớp trừu tượng của tôi (về cơ bản là mã nguồn lớp được tạo) KHÔNG cung cấp phép tiêm setter phụ thuộc cho các phần tử danh sách, cũng không phải là hàm tạo nơi khởi tạo các phần tử danh sách (mà tôi đã cố thêm bằng tay).

Chỉ các thuộc tính lớp sử dụng khởi tạo mặc định: Danh sách riêng dep1 = new ArrayList; Danh sách riêng dep2 = ArrayList mới

Vì vậy, KHÔNG có cách nào để giả định một lớp trừu tượng mà không sử dụng triển khai đối tượng thực (ví dụ: định nghĩa lớp bên trong trong lớp kiểm tra đơn vị, ghi đè các phương thức trừu tượng) và theo dõi đối tượng thực (khởi tạo trường thích hợp).

Thật tệ là chỉ PowerMock mới có thể giúp thêm ở đây.


2

Giả sử các lớp kiểm tra của bạn nằm trong cùng một gói (dưới một nguồn gốc khác) như các lớp của bạn đang kiểm tra, bạn có thể chỉ cần tạo giả:

YourClass yourObject = mock(YourClass.class);

và gọi các phương thức bạn muốn kiểm tra giống như bất kỳ phương thức nào khác.

Bạn cần cung cấp kỳ vọng cho từng phương thức được gọi với kỳ vọng vào bất kỳ phương thức cụ thể nào gọi phương thức siêu - không chắc bạn sẽ làm điều đó với Mockito như thế nào, nhưng tôi tin rằng điều đó có thể với EasyMock.

Tất cả điều này đang làm là tạo ra một ví dụ cụ thể YouClassvà tiết kiệm cho bạn nỗ lực cung cấp các triển khai trống của từng phương thức trừu tượng.

Bên cạnh đó, tôi thường thấy hữu ích khi triển khai lớp trừu tượng trong thử nghiệm của mình, trong đó nó đóng vai trò là một triển khai ví dụ mà tôi kiểm tra qua giao diện chung của nó, mặc dù điều này phụ thuộc vào chức năng được cung cấp bởi lớp trừu tượng.


3
Nhưng sử dụng giả sẽ không kiểm tra các phương thức cụ thể của YourClass, hay tôi sai? Đây không phải là những gì tôi tìm kiếm.
ripper234

1
Điều đó đúng, ở trên sẽ không hoạt động nếu bạn muốn gọi các phương thức cụ thể trên lớp trừu tượng.
Richard Nichols

Xin lỗi, tôi sẽ chỉnh sửa một chút về kỳ vọng, điều cần thiết cho mỗi phương thức bạn gọi không chỉ là phương pháp trừu tượng.
Nick Holt

nhưng sau đó bạn vẫn đang thử nghiệm bản giả của mình chứ không phải các phương pháp cụ thể.
Jonatan Cloutier

2

Bạn có thể mở rộng lớp trừu tượng với một lớp ẩn danh trong bài kiểm tra của bạn. Ví dụ: (sử dụng Junit 4):

private AbstractClassName classToTest;

@Before
public void preTestSetup()
{
    classToTest = new AbstractClassName() { };
}

// Test the AbstractClassName methods.

2

Mockito cho phép chế nhạo các lớp trừu tượng bằng cách @Mockchú thích:

public abstract class My {

    public abstract boolean myAbstractMethod();

    public void myNonAbstractMethod() {
        // ...
    }
}

@RunWith(MockitoJUnitRunner.class)
public class MyTest {

    @Mock(answer = Answers.CALLS_REAL_METHODS)
    private My my;

    @Test
    private void shouldPass() {
        BDDMockito.given(my.myAbstractMethod()).willReturn(true);
        my.myNonAbstractMethod();
        // ...
    }
}

Nhược điểm là nó không thể được sử dụng nếu bạn cần các tham số của hàm tạo.


0

Bạn có thể khởi tạo một lớp ẩn danh, tiêm giả của bạn và sau đó kiểm tra lớp đó.

@RunWith(MockitoJUnitRunner.class)
public class ClassUnderTest_Test {

    private ClassUnderTest classUnderTest;

    @Mock
    MyDependencyService myDependencyService;

    @Before
    public void setUp() throws Exception {
        this.classUnderTest = getInstance();
    }

    private ClassUnderTest getInstance() {
        return new ClassUnderTest() {

            private ClassUnderTest init(
                    MyDependencyService myDependencyService
            ) {
                this.myDependencyService = myDependencyService;
                return this;
            }

            @Override
            protected void myMethodToTest() {
                return super.myMethodToTest();
            }
        }.init(myDependencyService);
    }
}

Hãy nhớ rằng khả năng hiển thị phải protecteddành cho thuộc tính myDependencyServicecủa lớp trừu tượng ClassUnderTest.


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.