如何在使用Mockito测试我的SpringBoot应用时,从属性文件中传递变量?

huangapple go评论75阅读模式
英文:

How to pass a variable from properties file when testing my SpringBoot app with Mockito?

问题

SOLUTION

替代在DiceRollerService类中的"path"字段上使用注解@Value("${path.to.randomizer}"),我将这个注解传递给了类的构造函数:

@Service
public class DiceRollerService {
    private String path;
    private final CustomHttpClient client;

    @Autowired
    public DiceRollerService(CustomHttpClient client, @Value("${path.to.randomizer}") String path) {
        this.client = client;
        this.path = path;
    }
}

在这种情况下,如果我使用默认构造函数,"path"字段将链接到应用程序属性中的相应值,否则,如果我使用这个特定的构造函数,我可以通过它传递特定的路径值。以下是现在我的单元测试的样子:

@RunWith(MockitoJUnitRunner.class)
public class DiceRollerTest {
    private static final String PATH_FOR_TEST = "Some URL path";
    private static final int DICE_NUMBER_ONE = 2;
    private static final int DICE_NUMBER_TWO = 2;

    @Mock
    private CustomHttpClient client;

    private DiceRollerService diceRollerService;

    @Before
    public void init() {
        diceRollerService = new DiceRollerService(client, PATH_FOR_TEST);
    }

    @Test
    public void testDiceRollerServiceWithCorrectPathAndBody() {
        Dices expected = new Dices(DICE_NUMBER_ONE, DICE_NUMBER_TWO);
        when(client.getResponseBodyFromClient(PATH_FOR_TEST)).thenReturn(BODY_TEST);
        Dices actual = diceRollerService.roll();
        Assert.assertEquals(expected, actual);
        verify(client).getResponseBodyFromClient(anyString());
    }
}

INITIAL PROBLEM

我需要关于我的小项目的单元测试和/或集成测试方面的一些帮助。我有一个只有一个重要方法来返回一个"Dice"模型的类:

@Service
public class DiceRollerService {
    @Value("${path.to.randomizer}")
    private String path;
    private final static int FIRST_DICE_NUMBER_POS = 0;
    private final static int SECOND_DICE_NUMBER_POS = 2;
    private final CustomHttpClient client;

    public DiceRollerService(CustomHttpClient client) {
        this.client = client;
    }

    public Dices roll() {
        String results = client.sendGetRequest(path).body();
        int firstNumber = Integer.parseInt(String.valueOf(results.charAt(FIRST_DICE_NUMBER_POS)));
        int secondNumber = Integer.parseInt(String.valueOf(results.charAt(SECOND_DICE_NUMBER_POS)));
        return new Dices(firstNumber, secondNumber);
    }

    public void setPath(String path) {
        this.path = path;
    }
}

正如你所看到的,在"roll()"方法中,我调用client.sendGetRequest(path)来获取一个HttpResponse的正文,并从中提取一些数据,以便构建并返回一个"Dice"模型。除此之外没有什么复杂的,除了实际的路径位于我的属性文件src/main/resources/application.properties中。
后来,我决定为我的项目添加一些测试来检查这个逻辑:

public class DiceRollerTest {
    @Mock
    private CustomHttpClient client;

    @InjectMocks
    private DiceRollerService diceRollerService;

    @Before
    public void init() {
        MockitoAnnotations.initMocks(this);
    }

    @Test
    public void test() {
        Dices expected = new Dices(2, 2);
        when(client.sendGetRequest(anyString()).body()).thenReturn("2 2 ");
        Dices actual = diceRollerService.roll();
        Assert.assertEquals(expected, actual);
        verify(client, times(1)).sendGetRequest(anyString()).body();
    }
}

不幸的是,结果我得到了一个空指针异常在这一行:when(client.sendGetRequest(anyString()).body()).thenReturn("2 2 ");
看起来我的模拟不像我预期的那样工作。以下是所有我的依赖项:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
        <exclusions>
            <exclusion>
                <groupId>org.junit.vintage</groupId>
                <artifactId>junit-vintage-engine</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.datatype</groupId>
        <artifactId>jackson-datatype-jsr310</artifactId>
    </dependency>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.13</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.mockito</groupId>
        <artifactId>mockito-core</artifactId>
        <version>3.3.3</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.mockito</groupId>
        <artifactId>mockito-inline</artifactId>
        <version>3.3.3</version>
        <scope>test</scope>
    </dependency>
</dependencies>

是否有任何方法可以在单元测试中使用我的属性?我如何正确解决这个问题?非常感谢您提供的任何建议。提前谢谢。

英文:

SOLUTION

Instead of annotating the field "path" in DiceRollerService class with @Value("${path.to.randomizer}"), I passed the annotation to class constructor:

@Service
public class DiceRollerService {
private String path;
private final CustomHttpClient client;

@Autowired
public DiceRollerService(CustomHttpClient client, @Value(&quot;${path.to.randomizer}&quot;) String path) {
    this.client = client;
    this.path = path;
}

In this case, if I use the default constructor, the path field will be linked to the corresponding value from application properties, otherwise, if I use this specific constructor, I can pass the specific path value through it. Here is how my unit-test looks now:

@RunWith(MockitoJUnitRunner.class)
public class DiceRollerTest {
private static final String PATH_FOR_TEST = &quot;Some URL path&quot;;
private static final int DICE_NUMBER_ONE = 2;
private static final int DICE_NUMBER_TWO = 2;

@Mock
private CustomHttpClient client;

private DiceRollerService diceRollerService;

@Before
public void init() {
    diceRollerService = new DiceRollerService(client, PATH_FOR_TEST);
}

@Test
public void testDiceRollerServiceWithCorrectPathAndBody() {
    Dices expected = new Dices(DICE_NUMBER_ONE, DICE_NUMBER_TWO);
 when(client.getResponseBodyFromClient(PATH_FOR_TEST)).thenReturn(BODY_TEST);
    Dices actual = diceRollerService.roll();
    Assert.assertEquals(expected, actual);
    verify(client).getResponseBodyFromClient(anyString());
}

INITIAL PROBLEM

I need some assistance with unit- and/or integration-testing of my small project. I have a class that has only one important method to return a Dice model:

@Service
public class DiceRollerService {
    @Value(&quot;${path.to.randomizer}&quot;)
    private String path;
    private final static int FIRST_DICE_NUMBER_POS = 0;
    private final static int SECOND_DICE_NUMBER_POS = 2;
    private final CustomHttpClient client;

public DiceRollerService(CustomHttpClient client) {
    this.client = client;
}

public Dices roll() {
    String results = client.sendGetRequest(path).body();
    int firstNumber = Integer.parseInt(String.valueOf(results.charAt(FIRST_DICE_NUMBER_POS)));
    int secondNumber = Integer.parseInt(String.valueOf(results.charAt(SECOND_DICE_NUMBER_POS)));
    return new Dices(firstNumber, secondNumber);
}

public void setPath(String path) {
    this.path = path;
}
}

As you can see, in method roll() I invoke client.sendGetRequest(path) to get a HttpResponse' body and to fetch some data from it in order to build and return a Dice model. Nothing complicated except that the actual path is located at my properties file at src/main/resources/application.properties.
Later on, i decided to add some tests for my project to check the logic:

public class DiceRollerTest {
@Mock
private CustomHttpClient client;

@InjectMocks
private DiceRollerService diceRollerService;

@Before
public void init() {
    MockitoAnnotations.initMocks(this);
}

@Test
public void test() {
    Dices expected = new Dices(2, 2);
    when(client.sendGetRequest(anyString()).body()).thenReturn(&quot;2 2 &quot;);
    Dices actual = diceRollerService.roll();
    Assert.assertEquals(expected, actual);
    verify(client, times(1)).sendGetRequest(anyString()).body();
}
}

Unfortunately, as a result a got a nullpointer at this line: when(client.sendGetRequest(anyString()).body()).thenReturn("2 2 ");
Seems that my mock doesn't work as I expected. Here are all my dependencies :

&lt;dependencies&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
        &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt;
    &lt;/dependency&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
        &lt;artifactId&gt;spring-boot-starter-test&lt;/artifactId&gt;
        &lt;scope&gt;test&lt;/scope&gt;
        &lt;exclusions&gt;
            &lt;exclusion&gt;
                &lt;groupId&gt;org.junit.vintage&lt;/groupId&gt;
                &lt;artifactId&gt;junit-vintage-engine&lt;/artifactId&gt;
            &lt;/exclusion&gt;
        &lt;/exclusions&gt;
    &lt;/dependency&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;com.fasterxml.jackson.datatype&lt;/groupId&gt;
        &lt;artifactId&gt;jackson-datatype-jsr310&lt;/artifactId&gt;
    &lt;/dependency&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;junit&lt;/groupId&gt;
        &lt;artifactId&gt;junit&lt;/artifactId&gt;
        &lt;version&gt;4.13&lt;/version&gt;
        &lt;scope&gt;test&lt;/scope&gt;
    &lt;/dependency&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;org.mockito&lt;/groupId&gt;
        &lt;artifactId&gt;mockito-core&lt;/artifactId&gt;
        &lt;version&gt;3.3.3&lt;/version&gt;
        &lt;scope&gt;test&lt;/scope&gt;
    &lt;/dependency&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;org.mockito&lt;/groupId&gt;
        &lt;artifactId&gt;mockito-inline&lt;/artifactId&gt;
        &lt;version&gt;3.3.3&lt;/version&gt;
        &lt;scope&gt;test&lt;/scope&gt;
    &lt;/dependency&gt;
&lt;/dependencies&gt;

Is there any way I can use my properties in unit-testing? How can I properly fix that? Will appreciate any piece of advice. Thanks in advance.

答案1

得分: 1

这一行

when(client.sendGetRequest(anyString()).body()).thenReturn("2 2 ");

链接得太远了。你需要先对 client.sendGetRequest(anyString()) 的响应进行存根(stub),然后再对 .body() 调用进行存根。可以像这样处理:

when(client.sendGetRequest(anyString())).thenReturn(myMockedHttpResponse);
when(myMockedHttpResponse.body()).thenReturn("2 2 ");

注入属性是一个不同的问题;考虑将属性文件放在 /src/test/resources 下,或者查看 @TestPropertySource,或者重构你的属性为 @ConfigurationProperties 类,并注入其中的一个或... 无限的可能性 如何在使用Mockito测试我的SpringBoot应用时,从属性文件中传递变量?

英文:

this line

 when(client.sendGetRequest(anyString()).body()).thenReturn(&quot;2 2 &quot;);

is chained one step too far. You need to first stub the response of client.sendGetRequest(anyString()) before you can stub the .body() call. Something like

 when(client.sendGetRequest(anyString()).thenReturn(myMockedHttpResponse); 
 when(myMockedHttpResponse.body()).thenReturn(&quot;2 2 &quot;);

Injecting properties is a different problem; consider putting a properties file under /src/test/resources or have a look at @TestPropertySource or refactor your properties to @ConfigurationProperties-classes and inject one of those or... Endless possibilities 如何在使用Mockito测试我的SpringBoot应用时,从属性文件中传递变量?

答案2

得分: 0

<dependency>
    <groupId>com.github.exabrial</groupId>
    <artifactId>mockito-object-injection</artifactId>
    <version>2.0.2</version>
    <scope>test</scope>
</dependency>

它的工作原理是,在你的测试中创建一个与你要测试的服务或控制器中的目标字段同名的字段。用 @InjectionSource 注解标记此字段,然后在调用被测试类之前将其设置为你想要的任何值。非常简单:

@ExtendWith({ MockitoExtension.class, InjectExtension.class })
class MyControllerTest {
    @InjectMocks
    private MyController myController;
    @Mock
    private Logger log;
    @Mock
    private Authenticator auther;
    @InjectionSource
    private Boolean securityEnabled;

    @Test
    void testDoSomething_secEnabled() throws Exception {
        securityEnabled = Boolean.TRUE;
        myController.doSomething();
        // 没有 NPE!测试分支的 "if then" 部分
    }

    @Test
    void testDoSomething_secDisabled() throws Exception {
        securityEnabled = Boolean.FALSE;
        myController.doSomething();
        // 没有 NPE!测试分支的 "if else" 部分
    }
}
英文:

Faced with this exact problem, we developed a Test Extension that solves this exact problem very gracefully via integration with @InjectMocks: https://github.com/exabrial/mockito-object-injection

&lt;dependency&gt;
 &lt;groupId&gt;com.github.exabrial&lt;/groupId&gt;
 &lt;artifactId&gt;mockito-object-injection&lt;/artifactId&gt;
 &lt;version&gt;2.0.2&lt;/version&gt;
 &lt;scope&gt;test&lt;/scope&gt;
&lt;/dependency&gt;

How it works is you create a field in your test named the same thing as the target field in your service or controller under test. Mark this field with an @InjectionSource annotation, then set it to whatever you want before calling the class-under-test. Pretty simple:

@ExtendWith({ MockitoExtension.class, InjectExtension.class })
class MyControllerTest {
 @InjectMocks
 private MyController myController;
 @Mock
 private Logger log;
 @Mock
 private Authenticator auther;
 @InjectionSource
 private Boolean securityEnabled;
 
 @Test
 void testDoSomething_secEnabled() throws Exception {
  securityEnabled = Boolean.TRUE;
  myController.doSomething();
  // wahoo no NPE! Test the &quot;if then&quot; half of the branch
 }
 
 @Test 
 void testDoSomething_secDisabled() throws Exception {
  securityEnabled = Boolean.FALSE;
  myController.doSomething();
  // wahoo no NPE! Test the &quot;if else&quot; half of branch
 }
}

huangapple
  • 本文由 发表于 2020年9月21日 23:27:31
  • 转载请务必保留本文链接:https://go.coder-hub.com/63995379.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定