Giter VIP home page Giter VIP logo

mock-in-bean's Introduction

Mock in Bean

@MockInBean and @SpyInBean are alternatives to @MockBean and @SpyBean for Spring Boot 2 tests (>= 2.2.0).

They surgically replace a field value in a Spring Bean by a Mock/Spy for the duration of a test and set back the original value afterwards, leaving the Spring Context clean.

@MockInBean 'mocks a bean in a bean' whereas @MockBean 'mocks a bean in the whole context'.

But why ?

The problem:

Spring Context pollution was a fairly common problem before the introduction of @MockBean and @SpyBean. Many developers would inject mocks in beans during tests through @InjectMock or using manual setters and often forget to set back the original field values which would leave the Spring context polluted and cause test failures in unrelated tests.

@MockBean and @SpyBean solved this issue by providing Mockito injection directly in the Spring Context but introduced an undesirable side-effect: their usage dirties the context and may lead to the re-creation of new Spring contexts for any unique combination, which can be incredibly time-consuming. See The Problems with @MockBean

The solution:

Assuming you really need to run the test in the Spring Context, the most straight-forward solution is still to inject your mock/spy in your bean and reset it afterwards.

@MockInBean and @SpyInBean brings the convenience of @MockBean and @SpyBean and do just that:

  1. Set the mock/spy in the bean.
  2. Replace the mock/spy by the original value afterwards.

Example:

Assuming that you want to test the following service:

@Service
public class MyService {

    @Autowired
    protected ThirdPartyApiService thirdPartyService;

    @Autowired
    protected ExpensiveProcessor expensiveProcessor;

    public void doSomething() {
        final Object somethingExpensive = expensiveProcessor.returnSomethingExpensive();
        thirdPartyService.doSomethingOnThirdPartyApi(somethingExpensive);
    }

}

You can write your test this way:

@SpringBootTest
public class MyServiceTest {

    @MockInBean(MyService.class)
    private ThirdPartyApiService thirdPartyApiService;

    @SpyInBean(MyService.class)
    private ExpensiveProcessor expensiveProcessor;

    @Autowired
    private MyService myService;

    @Test
    public void test() {
        final Object somethingExpensive = new Object();
        Mockito.when(expensiveProcessor.returnSomethingExpensive()).thenReturn(somethingExpensive);
        myService.doSomething();
        Mockito.verify(thirdPartyApiService).doSomethingOnThirdPartyApi(somethingExpensive);
    }

}

What happens:

Before each test:

  • MyServiceTest.thirdPartyService will be created as a mock and injected in the target of @MockInBean: MyService.
  • MyServiceTest.expensiveProcessor will be created as a spy of the bean expensiveProcessor and injected in the target of @SpyInBean: MyService.

After the tests of MyServiceTest are done:

  • MyService.thirdPartyService will be reset to the original Spring bean thirdPartyService
  • MyService.expensiveProcessor will be reset to the original Spring bean expensiveProcessor

Usage:

Simply include the maven dependency (from central maven) to start using @MockInBean and @SpyInBean in your tests.

<dependency>
  <groupId>com.teketik</groupId>
  <artifactId>mock-in-bean</artifactId>
  <version>boot2-v1.5.2</version>
  <scope>test</scope>
</dependency>

@MockInBean and @SpyInBean also support:

  • Injection in multiple Spring beans: Repeat the annotation on your field.
  • Injection in bean identified by name if multiple instances exist in the context: Specify a name in your annotation.

Checkout the javadoc for more information.

Limitations:

This approach has some limitations compared to the @MockBean/@SpyBean equivalent:

  • There is currently no isolation per thread. It is not advised to use this library in parallel test suites. (Parallel test execution is supported experimentally in https://github.com/antoinemeyer/mock-in-bean/tree/v2.1-RC).
  • Operations on beans happening within another bean's constructor will not be performed against the mocks since the mocks are injected directly into the fields. Do not use @MockInBean/@SpyInBean for beans that are manipulated in constructors.

mock-in-bean's People

Contributors

antoinemeyer avatar maximilianwiedemann avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

mock-in-bean's Issues

Context gets corrupted with "nulls"

When I run tests and inspect the beans retrieved in the application context with the debugger, they all have null fields.
After running a few tests my context gets corrupted by the many "nulls" that get injected back as "original values".
I really don't understand what I got wrong...

ClassCastException with parameterized Type

If we use generic on @MockInBean we have ClassCastException on line

final Class<?> mockOrSpyType = (Class<?>) definition.getResolvableType().getType();

Because definition.getResolvableType().getType() return java.lang.reflect.ParameterizedType and not a class.

code must be change to check type and if returned value is instance of java.lang.reflect.ParameterizedType we must call
((java.lang.reflect.ParameterizedType)definition.getResolvableType().getType()).getRawType() to get Class

we can replace
final Class<?> mockOrSpyType = (Class<?>) definition.getResolvableType().getType();

with

final Class<?> mockOrSpyType = getClassOfType(definition.getResolvableType().getType()); 

private Class<?> getClassOfType(Type type) {
    Type result = type;
    if (type instanceof ParameterizedType) {
        result = ((ParameterizedType)definition.getResolvableType().getType()).getRawType();
    }
    return (Class<?>)result;

Moking a bean is null in Nested class

Hello.
Moking a bean is null in Nested class.

@SpringBootTest
public class NestedTest {

  @Autowired
  MyService target;

  @MockInBean(MyService.class)
  ThirdPartyApiService thirdService;

  String mockResult = "mock";

  @Test
  void resultReturnMockedValue() {
    when(thirdService.print()).thenReturn(mockResult);
    assertEquals(mockResult, target.invokeThirdPartySomeMethod());
  }

  @Nested
  class Inner {

    @Test
    void resultReturnMockedValue() {
      // java.lang.NullPointerException because "this.this$0.thirdService" is null
      when(thirdService.print()).thenReturn(mockResult);
      assertEquals(mockResult, target.invokeThirdPartySomeMethod());
    }
  }
}


@Service
class MyService {

  @Autowired
  public ThirdPartyApiService thirdService;

  public String invokeThirdPartySomeMethod() {
    return thirdService.print();
  }

}


@Service
class ThirdPartyApiService {

  public String print() {
    return "noMock";
  }

}

The resultReturnMockedValue in the NestedTest class is working well.
But, The resultReturnMockedValue in the Inner class which is Inner class of NestedTest class throw NullPointerException.

I want to mock in nested class.

I am using this version

		<dependency>
			<groupId>com.teketik</groupId>
			<artifactId>mock-in-bean</artifactId>
			<version>boot2-v1.4</version>
			<scope>test</scope>
		</dependency>


Mocking a bean in a Spy does not always work

Sometimes works and sometimes does not because of the non-deterministic aspect of MockInBean / SpyInbean definition parsing.

  • Works when the mock is created before the spy (because the spy will use the fields already injected into the bean).
  • Does not work when the spy is created before the mock (because the mock will be injected inside the spring bean and not the spy).

This is because the Spy does not delegate to the original object but creates a copy (as documented in https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Spy.html).

Could be fixed by also injecting mocks into spies.

Nested test classes that do not use MockInBean fail

Not every Spring boot integration test will necessarily use a MockBean/MockInBean, however when including the mock-in-bean dependency for the sake of those we do want to use it for, the MockInBeanTestExecutionListener will intercept every test rather than just ones needed to be used. This causes an issue with Nested SpringBootTest tests that do not use any Mocks, as ((LinkedList<FieldState>) applicableTestContext.getAttribute(ORIGINAL_VALUES_ATTRIBUTE_NAME)) will fail as applicableTestContext is null (because we didn't set it up in beforeTestClass). I don't think forcing a test listener is the right solution anyway, it should be able to be disabled on tests if wished...

long term suggestion - make it a test extension (rather than test listener) that implements BeforeEachCallback and AfterEachCallback and is then enabled on a per-test base using @ExtendWith(MockInBeanTestExtension.class), so it doesn't interfere with every test. short term solution - return if applicableTestContext is null

Support for interface defined beans

Hi,
I like your project, but I'm having some issues with it.
E.g. I have an interface called Service and its implementation ServiceImpl. If I want to inject Mock or Spy into this bean but it doesn't work.
Could you make it work with interface defined beans. I think the problem is that BeanUtils.findField doesn't search for the field in actual bean instance but on interface.

java.lang.IllegalArgumentException: Cannot find bean to mock interface com.Processor in interface com.Service 
	at org.springframework.util.Assert.notNull(Assert.java:219)
	at com.teketik.test.mockinbean.MockInBeanTestContextManager$Context$Builder.processDefinitions(MockInBeanTestContextManager.java:125)
	at com.teketik.test.mockinbean.MockInBeanTestContextManager$Context$Builder.<init>(MockInBeanTestContextManager.java:109)
	at com.teketik.test.mockinbean.MockInBeanTestContextManager$Context$Builder.<init>(MockInBeanTestContextManager.java:90)
class Test {
    
    @Autowired
    private Service service;

    @SpyInBean(Service.class)
    private Processor processor;

...

Mocks injection doesn't work for AOP proxies

When AOP is used Spring wraps the initial class with a JDK dynamic proxy or a CGLIB proxy. So bean found for mocks injection is actually a proxy and reflection-based injection doesn't have any effect. It is quite easy to fix in BeanFieldState.resolveTarget: return AopUtils.isAopProxy(bean) ? AopProxyUtils.getSingletonTarget(bean) : bean;

Let me know if PR is needed.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.