• 10 heures
  • Moyenne

Ce cours est visible gratuitement en ligne.

course.header.alt.is_video

course.header.alt.is_certifying

J'ai tout compris !

Mis à jour le 04/01/2021

Identify which integrations you are validating

How much imagination do our tests need?

Do you remember playing as a child? It’s a combination of imagination and props. If your goal was to have a laser sword fight, it helped to use an actual sword - or at the very least a stick. Costumes, sets, and flying droids can just be imagined. If you made them for real, you’d then never have had time to do any actual playing!

When you test your software using an integration test, you have to make similar compromises. It would be great to automate real humans to use your system every time you made a change! You’d definitely build the right thing! Manual testing tries to do this, but the reality is that it can slow down your whole development process. To strike a balance, we automate integration tests, using just the right mix of props (or real collaborators) to the imagination (or test doubles).

Let’s look at the different types of tests you could write and explore some examples.

Component integration tests

You’ve seen a lot of unit tests so far. Often using mocks. Component integration tests are pretty much the same thing, but you might not use a mock. A good way to understand this is by replacing the word “component” with “unit.” Does unit integration add some clarity?

You are testing the integration (the putting together) of the units you built and unit tested. As you saw in the earlier part of the course, naming things is one of the harder problems in software. It gets messed up a lot!

So let’s have a look at an example of such a test! We’re going to create an integration test for the CalculatorService, and make sure that it can be run anywhere.

Did you see how I named this CalculationServiceIT? The IT in the name stands for integration test. Ending tests in ‘IT,’ which is short for integration test, is a Java convention.

In terms of the actual test, it structurally resembles a unit test, but without mocks. Try to avoid retesting the CUT, instead, focus on getting assurance for some happy paths.

public class CalculatorServiceIT {

    // Set up REAL collaborators
    private Calculator calculator = new Calculator();
    private SolutionFormatter formatter = new SolutionFormatterImpl();
    
    // Set up the class under test
    private CalculatorService underTest = new CalculatorService(calculator, formatter);

    @Test
    public void calculatorService_shouldCalculateASolution_whenGivenACalculationModel() {
        // ARRANGE
        CalculationModel calculation = new CalculationModel(CalculationType.ADDITION,
                100, 101);
        // ACT
        CalculationModel result = underTest.calculate(calculation);

        // ASSERT
        assertThat(result.getSolution(), is(equalTo(201)));
    }
}

That’s it. Can you tell how it differs from the earlier unit tests? There’s not very much in there, is there? One thing which should stand out is that we didn't use any mocks on lines 4 and 5!

In such a test, you might have a happy path and handle an exception. You need just enough testing to have confidence that your units collaborate together. In our example, we focused on testing that a calculation is possible using real collaborators. The proof that we can actually do arithmetic has already been proven by our CalculatorTest, so there's no value in repeating that here.

If this works, you can trust your existing unit tests, and have greater confidence that the add, subtract, multiply, and divide methods all work when used by other classes!

Try it out for yourself!

Have a go at adding a missing component integration test which tests that exceptions thrown by our calculator are visible to our CalculatorService! Remember to write your test first, and make it go red, before you write the code! You can clone the repository and add your own code to it! You'll be using  @Test(exepcted=IllegalArgumentException.class)  and testing a scenario which attempts to divide by 0.

git clone https://github.com/OpenClassrooms-Student-Center/AchieveQualityThroughTestingInJava.git
git checkout p3-c2-sc1

Or use your IDE to clone the repository and check out the branch p3-c2-sc1. You can now explore the examples from the screencast and try running the tests with:  

mvn test

System integration test

I know that I made a big deal about calling these system integration tests, but they can also be thought of as broader component integration tests, with more components.

Most importantly, they involve pulling together the parts of your system which prove that your application will run when all the pieces come together. You can also write system integration tests to prove how your application communicates with another running application.

This might mean that you’d have confidence that your real web application might handle someone submitting a form because you’ve guessed what this would look like. This type of system integration is called a subcutaneous test and can test an otherwise end-to-end test without the front end.

You might also prove that your code could talk to a database by starting a portion of your application, and using an in-memory database.

Writing a system integration test for a Spring application

Spring is the go-to framework used by Java developers to build web and other types of applications. It helps glue all your classes into a working application. System integration tests help by testing a working system. Frameworks like Spring help pull classes together into just such a system!

Let’s imagine that our Calculator application has been modified by a teammate, who didn’t test first, as a Spring-based web application.

You can clone the repository here:

git clone https://github.com/dreamthought/openclassrooms-java-testing.git
git checkout p3-c2-sc3

Or use your IDE to clone the repository and check out the branch p3-c2-sc3. Run it with mvn spring-boot:run. You can then see it in your browser at localhost:8080. Have a play around.

Let me show you how I run it:

Now that you can follow along, I’m going to write a system integration test for it, in the video below:

I named the test CalculationControllerSIT to make it clearer that this is a system integration test. In the real world, you’ll often find both your component integration tests and system integration tests grouped together in the code. It is normal to end both of these types of test with the trailing IT. As I said, the only difference is the breadth of real collaborators you’re using.

Let’s have a look at the annotations we used:

@WebMvcTest(controllers = {CalculatorController.class, CalculatorService.class})
@RunWith(SpringJUnit4ClassRunner.class)
public class CalculatorControllerSIT {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private SolutionFormatter solutionFormatter;

    @MockBean
    private Calculator calculator;

    @Autowired
    private CalculatorService calculatorService;

    @Autowired
    private CalculatorController calculatorControllerUnderTest;

    @Test
     public void givenAStudentUsingTheApp_whenAreRequestIsMadeToAdd_thenASolutionSouldBeShown() throws Exception {
...

    }
}

Let’s get familiar with annotations we've used here.

@WebMvcTest
@WebMvcTest(controllers = {CalculatorController.class, CalculatorService.class})

Spring can set up a pretend environment that lets tests act as though you have a running web server. First, use the @WebMvcTest annotation on line 1 and pass it the real classes you want the Spring framework to help set up mocks for.

In this case, we're going to use a real CalculatorService and a real CalculatorController. In a web application, the controller class has methods that usually make it the first bit of code the outside world talks to.  In our code, it learns what it is you wanted to calculate, then gives a response back to your web browser in HTML. Since this is the class on the edge of our system that the browser talks to, it is where we’ll focus our system integration tests.

Use this annotation before a class declaration to tell Spring to help create instances of the specified classes and the mocks they need.

@RunWith(SpringJUnit4ClassRunner.class)

On line 2, you can see the  @RunWith  annotation. Here it is again: 

@RunWith(SpringJUnit4ClassRunner.class)

Like other class runners you've seen, the SpringJUnit4ClassRunner enhances JUnit tests so they provide handy features to help you mock and configure parts of your application.

@Autowired

Spring has the ability to create the classes your code needs so you don’t need to keep doing things like calling new CalculatorService(). The less routine code you can write in your tests, the more you can focus on testing that the public methods do what they are supposed to! 

@Autowired tells Spring to give you an instance of a class and handle creating it, like lines 5, 14 and 17 above. For example, the following gives a real instance of a CalculatorService ready to use in your tests:

@Autowired
private CalculatorService calculatorService;

The reason you can do this here is that CalculationService class is marked with @Service. This tells Spring that you need one instance of this class, and it tells the reader of the code that it provides methods other classes can use.

We call those instances which Spring creates, Spring beans. The term bean is used a lot in Java, and you shouldn’t confuse it with its many other meanings. In this case, it's just a class which Spring has created and configured for you, so you don't have to.

For Spring to be able to create a class for you, it would usually have to be marked with @Service,  @Component,  @Controller , or @Bean.

Here are a few examples of Spring beans which we can autowire from your calculator application:

@Controller
public class CalculatorController {
}

@Service
public class CalculatorService implements Solver {
}

@Component
public class Calculator {
}

@MockBean

On lines 8 and 11, we used the @MockBean annotation before the Calculator and SolutionFormatter fields.  Placing@MockBean before a field asks Spring's test runner to create a mock for us. 

    @MockBean
    private SolutionFormatter solutionFormatter;

    @MockBean
    private Calculator calculator;

By declaring  @MockBean Calculator calculator;  we are given a mocked instance of the Calculator to use within this test. It's just like Mockito's  @Mock, except that Spring will use this instance if it's needed by other  @Autowire declared classes. For instance, the field of type CalculatorService on line 14 requires an instance of calculator to be passed to its constructor. Spring will auto-magically give it the calculator mock we've created here. 

In our example, the CalculationService is a real instance of a CalculationService, but it happens to use the calculator mock, which we created above.

Marking the constructor of CalculationService with the  @Autowired  annotation asks Spring to look at the arguments passed to it:

    @Autowired
    public CalculatorService(Calculator calculator, SolutionFormatter formatter) {
        this.calculator = calculator;
        this.formatter = formatter;
    }

Basically, Spring will then find a class with an appropriate type and set it. In a Spring integration test, by marking a field as a MockBean, it becomes a candidate to fulfill any  @Autowired  dependencies in the system which is under test.

@Autowired MockMvc mockMvc

In line 6, Spring's test runner allows us to autowire a special class called MockMvc into our test. This is done just like the examples above, but it also gives us a special code-based web browser we can use to test a Spring app without starting the whole thing up. The MockMvc class will call your controller as though the application has actually been started and let you inspect how it responds in your tests.

Let’s have a look at the test!
    @Test
    public void givenAStudentUsingTheApp_whenAreRequestIsMadeToAdd_thenASolutionSouldBeShown() throws Exception {
        when(calculator.add(2,3)).thenReturn(5);
        mockMvc.perform(post("/calculator")
        .param("leftArgument", "2")
        .param("rightArgument", "3")
        .param("calculationType", "ADDITION")
        ).andExpect(status().is2xxSuccessful()).
                andExpect(content().string(containsString("id=\"solution\""))).
                andExpect(content().string(containsString(">5</span>")));


        verify(calculator).add(2, 3);
        verify(solutionFormatter).format(5);
    }

This is a subcutaneous test which mimics a user utilizing a web browser, without needing to launch the whole application or even a real browser! Our controller can respond to a request from a hypothetical browser which provides it with two numbers and a type of calculation.

In this case, line 4 used the fake mockMvc browser to pretend it's submitting a form.  param("leftArgument", 2)  and the next three rows submit form fields as though someone had done so within a browser.

We used the static post method on line 4 for this. The value "/calculator" should match up with another annotation in our CalculatorController marked  @PostMapping. Here's the actual code which a browser would interact with:

@PostMapping("/calculator")
public String calculate(@Valid Calculation calculation, BindingResult bindingResult, Model model) {
        // Arrange
        CalculationType type = CalculationType.valueOf(calculation.getCalculationType());
        CalculationModel calculationModel = new CalculationModel(
                type,
                calculation.getLeftArgument(),
                calculation.getRightArgument());
        // Act
        CalculationModel response = calculatorService.calculate(calculationModel);

        // Present
        model.addAttribute("response", response);
        return CALCULATOR_TEMPLATE;    
}

 When a test calls  mockMvc.perform(post("/calculator")), it is trying to connect with one of the methods in your controller. The post() method resembles the action of hitting a submit button on a form in a browser. The value you give it is called a path, and should match a method annotated in your controller with a @PostMapping and that path.  It matches up with  @PostMapping("/calculator") at line 1, above.

For other types of scenarios, you can get @GetMapping with the  get()  static method. And @PutMapping with the put() static method.

You can see that our controller resembles the test cases we wrote previously that call    CalulationService.calculate(calculationModel) as we have done on line 10.  This returns another CalculationModel with a solution. Spring then uses this model to show a response to the user. The code for this is in resources/templates/calculator.html.

Think of this as a web page that is used to take input from the user and show them the result. The file contains the following snippet, which only shows the solution if a response was calculated at line 2. This would replace the word Solution in line 4.

<div class="col">
    <ul th:if="${response}">
        <span id="solution" th:text="${response.getSolution()}" class="badge">
        Solution
        </span>
    </ul>
</div>

 Since our focus is on testing, I'll go back to our test. Lines 8 to 11 ensure that a good response would be returned to our mythical browser and that it contains the above snippet with the solution to 2+3 present where we have a span with the id of solution.

Spring’s MockMvc instance, which we declared earlier in our code, allows you to pretend that your test is a browser that sends data to the controller.  Without diving too far into how web browsers work, this test just means that you’re doing the same things a web browser would.

We did all of this without starting the application!

Try it out for yourself!

All this might seem a bit much the first time, but have a look at the code and try to follow it. There's really not much there. Check out the CalulatorControllerSIT and see if you can add a test to multiply two numbers.

You can clone the repository and add your own code to it!

git clone https://github.com/dreamthought/openclassrooms-java-testing.git
git checkout p3-c2-sc3

Or use your IDE to clone the repository and check out the branch p3-c2-sc3.

You can also explore the examples from the screencast and try running the tests with:

mvn test

Have a look at the README.md for tips!

Let's recap!

  • Component integration tests validate that previously tested units (the classes of your app) actually collaborate as their mocks did.

  • System integration tests are really just scaled up component integration tests, which test within a partially running system and examine integrations against other system level collaborators (like databases).

  • We wrote system integration tests which validated a Spring web application without a browser using @WebMvcTest and the SpringJUnit4ClassRunner JUnit runner.

Exemple de certificat de réussite
Exemple de certificat de réussite