Mockito 如何仅模拟超类方法的调用

发布于 2024-09-13 16:52:26 字数 348 浏览 5 评论 0原文

我在一些测试中使用 Mockito。

我有以下类:

class BaseService {  
    public void save() {...}  
}

public Childservice extends BaseService {  
    public void save(){  
        //some code  
        super.save();
    }  
}   

我只想模拟 ChildService 的第二次调用 (super.save)。第一次调用必须调用真正的方法。有办法做到这一点吗?

I'm using Mockito in some tests.

I have the following classes:

class BaseService {  
    public void save() {...}  
}

public Childservice extends BaseService {  
    public void save(){  
        //some code  
        super.save();
    }  
}   

I want to mock only the second call (super.save) of ChildService. The first call must call the real method. Is there a way to do that?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(11

书间行客 2024-09-20 16:52:30

就我而言,我创建了一个新方法来包装超类的方法并进行测试替身。因为当我实现 oAuth2 客户端时,我需要来自超类方法的结果。我希望这个答案对需要使用超类方法结果的人有帮助。

被测系统

  • 创建一个 loadUserFromParent 方法来包装超类函数
package action.in.blog.service;

import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class DefaultOAuth2UserServiceDelegator extends DefaultOAuth2UserService {

    private final List<CustomOAuth2UserService> oAuth2UserServices;


    public DefaultOAuth2UserServiceDelegator(List<CustomOAuth2UserService> oAuth2UserServices) {
        this.oAuth2UserServices = oAuth2UserServices;
    }

    public OAuth2User loadUserFromParent(OAuth2UserRequest userRequest) {
        return super.loadUser(userRequest);
    }

    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        for (var oauth2Service : oAuth2UserServices) {
            if (!oauth2Service.supports(userRequest)) {
                continue;
            }
            var oauthUser = loadUserFromParent(userRequest);
            return oauth2Service.createOrLoadUser(oauthUser);
        }
        throw new RuntimeException("Not found user service");
    }
}

测试

  • 使 SUT 作为间谍
  • 使用 doReturn(expectedValue).when(testDouble).method(params)
package action.in.blog.service;

import action.in.blog.domain.CustomAuthentication;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;

import java.util.Collections;
import java.util.List;
import java.util.Map;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.*;

class DefaultOAuth2UserServiceDelegatorTest {

    CustomOAuth2UserService googleUserService;
    CustomOAuth2UserService facebookUserService;
    DefaultOAuth2UserServiceDelegator sut;

    @BeforeEach
    void setUp() {
        googleUserService = mock(CustomOAuth2UserService.class);
        facebookUserService = mock(CustomOAuth2UserService.class);
        sut = spy(new DefaultOAuth2UserServiceDelegator(
                List.of(googleUserService, facebookUserService)
        ));
    }

    @Test
    void loadUser() {

        var authentication = new CustomAuthentication();
        var oauth2UserRequest = mock(OAuth2UserRequest.class);
        var oauth2User = new DefaultOAuth2User(Collections.emptyList(), Map.of("id", "junhyunny"), "id");
        // make a stub 
        doReturn(oauth2User).when(sut).loadUserFromParent(oauth2UserRequest);
        when(googleUserService.supports(oauth2UserRequest)).thenReturn(true);
        when(googleUserService.createOrLoadUser(oauth2User)).thenReturn(authentication);


        var result = sut.loadUser(oauth2UserRequest);


        assertEquals(result, authentication);
    }
}

In my case, I make a new method to wrap super class's method and make a test double. Because I need result from super class method when I implement oAuth2 client. I want this answer is helpful who someone needs to use result from super class's method.

System Under Test

  • Make a loadUserFromParent method to wrap super class function
package action.in.blog.service;

import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class DefaultOAuth2UserServiceDelegator extends DefaultOAuth2UserService {

    private final List<CustomOAuth2UserService> oAuth2UserServices;


    public DefaultOAuth2UserServiceDelegator(List<CustomOAuth2UserService> oAuth2UserServices) {
        this.oAuth2UserServices = oAuth2UserServices;
    }

    public OAuth2User loadUserFromParent(OAuth2UserRequest userRequest) {
        return super.loadUser(userRequest);
    }

    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        for (var oauth2Service : oAuth2UserServices) {
            if (!oauth2Service.supports(userRequest)) {
                continue;
            }
            var oauthUser = loadUserFromParent(userRequest);
            return oauth2Service.createOrLoadUser(oauthUser);
        }
        throw new RuntimeException("Not found user service");
    }
}

Test

  • Make SUT as a spy
  • Use doReturn(expectedValue).when(testDouble).method(params)
package action.in.blog.service;

import action.in.blog.domain.CustomAuthentication;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;

import java.util.Collections;
import java.util.List;
import java.util.Map;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.*;

class DefaultOAuth2UserServiceDelegatorTest {

    CustomOAuth2UserService googleUserService;
    CustomOAuth2UserService facebookUserService;
    DefaultOAuth2UserServiceDelegator sut;

    @BeforeEach
    void setUp() {
        googleUserService = mock(CustomOAuth2UserService.class);
        facebookUserService = mock(CustomOAuth2UserService.class);
        sut = spy(new DefaultOAuth2UserServiceDelegator(
                List.of(googleUserService, facebookUserService)
        ));
    }

    @Test
    void loadUser() {

        var authentication = new CustomAuthentication();
        var oauth2UserRequest = mock(OAuth2UserRequest.class);
        var oauth2User = new DefaultOAuth2User(Collections.emptyList(), Map.of("id", "junhyunny"), "id");
        // make a stub 
        doReturn(oauth2User).when(sut).loadUserFromParent(oauth2UserRequest);
        when(googleUserService.supports(oauth2UserRequest)).thenReturn(true);
        when(googleUserService.createOrLoadUser(oauth2User)).thenReturn(authentication);


        var result = sut.loadUser(oauth2UserRequest);


        assertEquals(result, authentication);
    }
}
风筝在阴天搁浅。 2024-09-20 16:52:30

有一种适用于大多数情况的简单方法。您可以监视您的对象并存根您想要模拟的方法。

这是一个示例:

MyClass myObjectSpy = Mockito.spy(myObject);
org.mockito.Mockito.doReturn("yourReturnValue").when(mySpyObject).methodToMock(any()..);

因此,当您测试对象时,可以使用 myObjectSpy ,并且当调用 methodToMock 时,它将通过模拟方法覆盖正常行为。

此代码用于带返回的方法。如果您有一个 void 方法,您可以使用 什么都不做 代替。

There is simple approach that works for most of cases. You can spy your object and stub the method you want to mock.

Here is an example:

MyClass myObjectSpy = Mockito.spy(myObject);
org.mockito.Mockito.doReturn("yourReturnValue").when(mySpyObject).methodToMock(any()..);

So, when you test your object, you can use myObjectSpy and when methodToMock is called, it will overwrite the normal behavior by a mock method.

This code for a method with return. In case you have a void method you can use doNothing instead.

蓝戈者 2024-09-20 16:52:29

您可以使用 PowerMockito 来执行此操作,并通过继续测试子类方法来仅替换父类方法的行为。即使该方法返回某个值(假设是一个字符串),您也可以执行以下操作:

@RunWith(PowerMockRunner.class)
@PrepareForTest({ BaseService.class })
public class TestChildService() {

    private BasicService basicServiceObj;
    private ChildService testee;

    @Before
    public void init() throws Exception {
        testee = new ChildService();
        basicServiceObj = PowerMockito.spy(new BaseService());
        PowerMockito.doReturn("Result").when(basicServiceObj, "save", ... optionalArgs);
    }

    @Test
    public void testSave(){
        testee.save();
    }
}

如果您没有返回任何值 (void),则可以使用 doReturn 来代替什么也不做。如果方法有一些参数,则添加一些 optionalArgs,如果没有,则跳过该部分。

You can do this with PowerMockito and replace behavior only of the parent class method with continuing testing the child's class method. Even when the method is returning some value, lets say a string, you can do something like this:

@RunWith(PowerMockRunner.class)
@PrepareForTest({ BaseService.class })
public class TestChildService() {

    private BasicService basicServiceObj;
    private ChildService testee;

    @Before
    public void init() throws Exception {
        testee = new ChildService();
        basicServiceObj = PowerMockito.spy(new BaseService());
        PowerMockito.doReturn("Result").when(basicServiceObj, "save", ... optionalArgs);
    }

    @Test
    public void testSave(){
        testee.save();
    }
}

If you are returning nothing (void) then instead of doReturn you can use doNothing. Add some optionalArgs if the method have some arguments, if not, then skip that part.

§普罗旺斯的薰衣草 2024-09-20 16:52:29

原因是你的基类不是公共的,那么Mockito由于可见性而无法拦截它,如果你将基类更改为公共,或者在子类中@Override(公共),那么Mockito可以正确模拟它。

public class BaseService{
  public boolean foo(){
    return true;
  }
}

public ChildService extends BaseService{
}

@Test
@Mock ChildService childService;
public void testSave() {
  Mockito.when(childService.foo()).thenReturn(false);

  // When
  assertFalse(childService.foo());
}

The reason is your base class is not public-ed, then Mockito cannot intercept it due to visibility, if you change base class as public, or @Override in sub class (as public), then Mockito can mock it correctly.

public class BaseService{
  public boolean foo(){
    return true;
  }
}

public ChildService extends BaseService{
}

@Test
@Mock ChildService childService;
public void testSave() {
  Mockito.when(childService.foo()).thenReturn(false);

  // When
  assertFalse(childService.foo());
}
逆光下的微笑 2024-09-20 16:52:28

我找到了一种使用 PowerMockito 抑制超类方法的方法。 需要3个简单的步骤

  1. 使用PowerMockito.suppress方法和MemberMatcher.methodsDeclaredIn方法来抑制父类方法

  2. 第二个在@PrepareForTest中添加父类

  3. 使用PowerMock运行测试类,即在测试类上方添加@RunWith(PowerMockRunner.class)

    @RunWith(PowerMockRunner.class)
    @PrepareForTest({BaseService.class})
    公共类 TestChildService(){
    
        @间谍
        私有 ChildService testChildServiceObj = Mockito.spy(new ChildService());
    
        @测试
        公共无效测试保存(){
            PowerMockito.suppress(MemberMatcher.methodsDeclaredIn(BaseService.class));
    
            //你的进一步测试代码
    
            testChildServiceObj.save();
        }
    }
    

注意:只有当超类方法不返回任何内容时,这才有效。

I found a way to suppress the superclass method using PowerMockito. 3 simple steps need for this

  1. Use PowerMockito.suppress method and MemberMatcher.methodsDeclaredIn method to supress parent class method

  2. Second add Parent class in @PrepareForTest

  3. Run your test class with PowerMock ie add @RunWith(PowerMockRunner.class) above your test class.

    @RunWith(PowerMockRunner.class)
    @PrepareForTest({BaseService.class})
    public class TestChildService(){
    
        @Spy
        private ChildService testChildServiceObj = Mockito.spy(new ChildService());
    
        @Test
        public void testSave(){
            PowerMockito.suppress(MemberMatcher.methodsDeclaredIn(BaseService.class));
    
            //your further test code
    
            testChildServiceObj.save();
        }
    }
    

Note: This will work only when the superclass method does not return anything.

仙女山的月亮 2024-09-20 16:52:28

如果继承有意义,也许最简单的选择是创建一个新方法(包私有?)来调用 super(让我们称之为 superFindall),监视真实实例,然后以您想要模拟的方式模拟 superFindAll() 方法家长一班。就覆盖范围和可见性而言,它不是完美的解决方案,但它应该可以完成工作并且易于应用。

 public Childservice extends BaseService {
    public void save(){
        //some code
        superSave();
    }

    void superSave(){
        super.save();
    }
}

Maybe the easiest option if inheritance makes sense is to create a new method (package private??) to call the super (lets call it superFindall), spy the real instance and then mock the superFindAll() method in the way you wanted to mock the parent class one. It's not the perfect solution in terms of coverage and visibility but it should do the job and it's easy to apply.

 public Childservice extends BaseService {
    public void save(){
        //some code
        superSave();
    }

    void superSave(){
        super.save();
    }
}
莫言歌 2024-09-20 16:52:28

在调用超类方法的子类中创建一个受包保护(假设测试类位于同一包中)的方法,然后在重写的子类方法中调用该方法。然后,您可以通过使用间谍模式在测试中设置对此方法的期望。不太漂亮,但肯定比在测试中处理 super 方法的所有期望设置要好

create a package protected (assumes test class in same package) method in the sub class that calls the super class method and then call that method in your overridden sub class method. you can then set expectations on this method in your test through the use of the spy pattern. not pretty but certainly better than having to deal with all the expectation setting for the super method in your test

笛声青案梦长安 2024-09-20 16:52:28

即使我完全同意 iwein 的回应(

优先考虑组合而不是继承

),我承认有时继承似乎很自然,并且我不觉得只是为了单元测试而破坏或重构它。

所以,我的建议:

/**
 * BaseService is now an asbtract class encapsulating 
 * some common logic callable by child implementations
 */
abstract class BaseService {  
    protected void commonSave() {
        // Put your common work here
    }

    abstract void save();
}

public ChildService extends BaseService {  
    public void save() {
        // Put your child specific work here
        // ...

        this.commonSave();
    }  
}

然后,在单元测试中:

    ChildService childSrv = Mockito.mock(ChildService.class, Mockito.CALLS_REAL_METHODS);

    Mockito.doAnswer(new Answer<Void>() {
        @Override
        public Boolean answer(InvocationOnMock invocation)
                throws Throwable {
            // Put your mocked behavior of BaseService.commonSave() here
            return null;
        }
    }).when(childSrv).commonSave();

    childSrv.save();

    Mockito.verify(childSrv, Mockito.times(1)).commonSave();

    // Put any other assertions to check child specific work is done

Even if i totally agree with iwein response (

favor composition over inheritance

), i admit there are some times inheritance seems just natural, and i don't feel breaking or refactor it just for the sake of a unit test.

So, my suggestion :

/**
 * BaseService is now an asbtract class encapsulating 
 * some common logic callable by child implementations
 */
abstract class BaseService {  
    protected void commonSave() {
        // Put your common work here
    }

    abstract void save();
}

public ChildService extends BaseService {  
    public void save() {
        // Put your child specific work here
        // ...

        this.commonSave();
    }  
}

And then, in the unit test :

    ChildService childSrv = Mockito.mock(ChildService.class, Mockito.CALLS_REAL_METHODS);

    Mockito.doAnswer(new Answer<Void>() {
        @Override
        public Boolean answer(InvocationOnMock invocation)
                throws Throwable {
            // Put your mocked behavior of BaseService.commonSave() here
            return null;
        }
    }).when(childSrv).commonSave();

    childSrv.save();

    Mockito.verify(childSrv, Mockito.times(1)).commonSave();

    // Put any other assertions to check child specific work is done
久随 2024-09-20 16:52:27

如果您确实没有重构的选择,您可以模拟/存根超级方法调用中的所有内容,例如

    class BaseService {

        public void validate(){
            fail(" I must not be called");
        }

        public void save(){
            //Save method of super will still be called.
            validate();
        }
    }

    class ChildService extends BaseService{

        public void load(){}

        public void save(){
            super.save();
            load();
        }
    }

    @Test
    public void testSave() {
        ChildService classToTest = Mockito.spy(new ChildService());

        // Prevent/stub logic in super.save()
        Mockito.doNothing().when((BaseService)classToTest).validate();

        // When
        classToTest.save();

        // Then
        verify(classToTest).load();
    }

If you really don't have a choice for refactoring you can mock/stub everything in the super method call e.g.

    class BaseService {

        public void validate(){
            fail(" I must not be called");
        }

        public void save(){
            //Save method of super will still be called.
            validate();
        }
    }

    class ChildService extends BaseService{

        public void load(){}

        public void save(){
            super.save();
            load();
        }
    }

    @Test
    public void testSave() {
        ChildService classToTest = Mockito.spy(new ChildService());

        // Prevent/stub logic in super.save()
        Mockito.doNothing().when((BaseService)classToTest).validate();

        // When
        classToTest.save();

        // Then
        verify(classToTest).load();
    }
云归处 2024-09-20 16:52:27

不,Mockito 不支持这一点。

这可能不是您正在寻找的答案,但您看到的是不应用设计原则的症状:

优先考虑组合而不是继承

如果您提取策略而不是扩展超类,那么问题就消失了。

然而,如果你不被允许更改代码,但无论如何你必须测试它,并且以这种尴尬的方式,仍然有希望。使用一些 AOP 工具(例如 AspectJ),您可以将代码编织到超类方法中并完全避免其执行(恶心)。如果您使用代理,则这不起作用,您必须使用字节码修改(加载时编织或编译时编织)。还有一些模拟框架也支持这种类型的技巧,例如 PowerMock 和 PowerMockito。

我建议你进行重构,但如果这不是一个选择,你就会享受一些严肃的黑客乐趣。

No, Mockito does not support this.

This might not be the answer you're looking for, but what you're seeing is a symptom of not applying the design principle:

Favor composition over inheritance

If you extract a strategy instead of extending a super class the problem is gone.

If however you are not allowed to change the code, but you must test it anyway, and in this awkward way, there is still hope. With some AOP tools (for example AspectJ) you can weave code into the super class method and avoid its execution entirely (yuck). This doesn't work if you're using proxies, you have to use bytecode modification (either load time weaving or compile time weaving). There are be mocking frameworks that support this type of trick as well, like PowerMock and PowerMockito.

I suggest you go for the refactoring, but if that is not an option you're in for some serious hacking fun.

爱你不解释 2024-09-20 16:52:27

考虑将代码从 ChildService.save() 方法重构为不同的方法,并测试新方法而不是测试 ChildService.save(),这样您将避免不必要的超级方法调用。

例子:

class BaseService {  
    public void save() {...}  
}

public Childservice extends BaseService {  
    public void save(){  
        newMethod();    
        super.save();
    }
    public void newMethod(){
       //some codes
    }
} 

Consider refactoring the code from ChildService.save() method to different method and test that new method instead of testing ChildService.save(), this way you will avoid unnecessary call to super method.

Example:

class BaseService {  
    public void save() {...}  
}

public Childservice extends BaseService {  
    public void save(){  
        newMethod();    
        super.save();
    }
    public void newMethod(){
       //some codes
    }
} 
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文