【问题标题】:How do you configure mocks before Autowired?在 Autowired 之前如何配置模拟?
【发布时间】:2020-06-27 15:09:29
【问题描述】:

我想在测试前使用 Mockito 设置模拟数据。但是,自动连线发生在@Before 之前,所以我的预期数据存在的测试丢失了。有办法解决吗?

@RunWith(SpringRunner.class)
@ContextConfiguration(
    classes = {
       Foo.class
    }
)
public class FooTest {
    @MockBean
    final Programs programs;
    @Autowired
    final Foo foo;

    @Before
    public void setPrograms() {

        when(programs.findAll())
            .thenReturn(
                List.of(
                    "A", "B", "C"
                )
            );

    }

    @Test
    public void foo() {
      assertThat(foo.getBlah()).isNotEmpty();
    }
}

interface Programs {
  List<String> findAll();
}

class Foo {

  // I have more complicated structures than a list, for example only.
  private List<String> blah;
  @Autowired
  private Programs programs;

  public List<String> getBlah() { return blah; }

  @PostConstruct
  public void init() {
    blah = programs.findAll();
  }
}

【问题讨论】:

  • 您能否提供一个示例来说明您正在做什么并试图实现的目标?因为 Mockito 提供了像 whengiven 这样的方法来指定测试中应该返回哪些方法(或属性的 getter)

标签: spring testing mocking


【解决方案1】:

按照你的例子,我开发了下一个:

public interface Programs {
  List<String> findAll();
}


public class Foo {

  // I have more complicated structures than a list, for example only.
  private List<String> blah;

  @Autowired
  private Programs programs;

  public List<String> getBlah() { return blah; }

  @PostConstruct
  public void init() {
    blah = programs.findAll();
  }
}

以上等于您提供的代码。现在,“变化”:

// Only for testing purpose, you can use any of your own ones
public class ProgramsImpl implements Programs {

  @Override
  public List<String> findAll() {
    return asList("1", "2", "3");
  }
}

还有junit测试:

@RunWith(SpringRunner.class)
@ContextConfiguration(
    classes = {
            Foo.class, ProgramsImpl.class
    }
)
public class FooTest {

  @SpyBean
  Programs programs;

  @Autowired
  Foo foo;

  @Before
  public void prepare() {
    when(programs.findAll())
            .thenReturn(
                    List.of(
                            "A", "B", "C"
                    )
            );
    this.foo.init();
  }

  @Test
  public void foo() {
    assertEquals(asList("A", "B", "C"), foo.getBlah());
  }
}

上述测试按预期工作,即尽管ProgramsImpl.findAll 返回"1", "2", "3",但包含的when 子句将用您自己的子句覆盖它。

但是,您可能会发现 @PostConstruct 确实被调用了两次:一次是由于@Autowire 而第二次是由于init

如果您想避免这种情况,您需要在测试中删除 @Autowire 并在 @Before 内创建您的 foo 自定义实例(使用“普通构造函数”)。例如:

public class Foo {
  ...
  private Programs programs;

  @Autowired
  public Foo(Programs programs) {
    this.programs = programs;
  }
  ...
}


@RunWith(SpringRunner.class)
@ContextConfiguration(
    classes = {
            ProgramsImpl.class
    }
)
public class FooTest {

  @SpyBean
  Programs programs;

  Foo foo;

  @Before
  public void prepare() {
    when(programs.findAll())
            .thenReturn(
                    List.of(
                            "A", "B", "C"
                    )
            );
    this.foo = new Foo(programs);
    this.foo.init();
  }

  @Test
  public void foo() {
    assertEquals(asList("A", "B", "C"), foo.getBlah());
  }

 }

【讨论】:

  • 正如您所注意到的,存在双重初始化问题,在 @Before 中创建的 Foo 也不是由 spring 管理的,这意味着它附带的注入不再起作用。我需要做的是创建一个单独的配置类来提供对象,我现在正在尝试为它提取我的示例。
  • 当然,当我这样做时,每个测试类只能有一组测试数据。
  • Of course, when I do that, I can only have one set of data for the test per test class 我只举了一个例子,你可以为你的测试生成你需要的东西。在每个(使用不同的when 子句)之前包含所需的自定义初始化,而不是对所有这些都以“全局方式”
  • also the Foo that is created in the @Before is not managed by spring => 这在 Junit 测试中不是必需的,我的意思是,通常你以你想要的方式创建主类并模拟相关的类(因为每个人都有自己的测试)。因此,在 Junit 中使用构造函数创建 Foo 的实例是合适的,因为它不会改变您真正想要测试的内容。
【解决方案2】:

因为Programs是一个接口;实际上它扩展了CrudRepository 我能够实例化它的一个副本,甚至是一个带有@Configuration/@Bean 的模拟。此解决方案确保所有内容(除了 mocks)都由 Spring 管理,包括 PostConstruct 生命周期。

这种方法允许我拥有包含使用 JPA 的 PostConstructs 的 Spring 组件,而无需使用适当的数据设置初始数据库。

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;

import javax.annotation.PostConstruct;
import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

@RunWith(SpringRunner.class)
@ContextConfiguration(
    classes = {
        Foo.class,
        StackOverflowTest.ProgramsConfiguration.class
    }
)
public class StackOverflowTest {
    @Autowired
    private Foo foo;

    @Configuration
    static class ProgramsConfiguration {

        @Bean
        public Programs programs() {

            final Programs mock = mock(Programs.class);
            when(mock.findAll()).thenReturn(List.of("A", "B", "C"));
            return mock;

        }

    }

    @Test
    public void foo() {

        assertThat(foo.getBlah())
            .isNotEmpty()
            .containsExactly("A", "B", "C");

    }
}

interface Programs {
    List<String> findAll();
}

class Foo {

    private List<String> blah;

    @Autowired
    private Programs programs;

    public List<String> getBlah() {

        return blah;
    }

    @PostConstruct
    public void init() {

        blah = programs.findAll();
    }
}

实际上,当我尝试让 JOOQ 与 PostConstructs 以及使用 MockDslContextConfiguration 一起工作时,我实际上发现我已经解决了这个问题。我花了一点时间才意识到这是同样的情况。

@Configuration
public class MockDslContextConfiguration {
    @Bean
    public DSLContext dslContext() {

        return DSL.using(new MockConnection(mockExecuteContext -> new MockResult[]{
            new MockResult(0, Mockito.mock(Result.class)),
            new MockResult(0, Mockito.mock(Result.class))
        }), SQLDialect.MYSQL);
    }
}

虽然这个问答更笼统,并不局限于 JOOQ。

这有一个限制,即每个测试类我只能有一组测试数据。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2022-07-06
    • 2021-06-06
    • 1970-01-01
    • 2021-03-28
    • 2020-03-04
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多