wake-up-neo.com

Wie man die Fernbedienung spottet REST API im Unit-Test mit Spring?

Angenommen, ich habe in meiner Anwendung einen einfachen Client erstellt, der einen Remote-Webdienst verwendet, der eine RESTful-API unter einem bestimmten URI /foo/bar/{baz} verfügbar macht. Jetzt möchte ich meinen Client testen, der diesen Webdienst anruft.

Idealerweise möchte ich in meinen Tests die Antworten, die ich vom Webservice bekomme, mit einer bestimmten Anfrage wie /foo/bar/123 oder /foo/bar/42 nachahmen. Mein Client geht davon aus, dass die API tatsächlich irgendwo ausgeführt wird. Daher brauche ich einen lokalen "Web-Service", um die Tests mit http://localhost:9090/foo/bar auszuführen.

Ich möchte, dass meine Komponententests unabhängig sind, ähnlich wie beim Testen von Federsteuerungen mit dem Spring MVC Test Framework. 

Einige Pseudo-Codes für einen einfachen Client, die Zahlen von der Remote-API abrufen:

// Initialization logic involving setting up mocking of remote API at 
// http://localhost:9090/foo/bar

@Autowired
NumberClient numberClient // calls the API at http://localhost:9090/foo/bar

@Test
public void getNumber42() {
    onRequest(mockAPI.get("/foo/bar/42")).thenRespond("{ \"number\" : 42 }");
    assertEquals(42, numberClient.getNumber(42));
}

// ..

Was sind meine Alternativen für Spring?

22
user1019830

Wenn Sie Spring RestTemplate verwenden, können Sie MockRestServiceServer verwenden. Ein Beispiel finden Sie hier https://objectpartners.com/2013/01/09/rest-client-testing-with-mockrestserviceserver/

12
Tuno

Am besten verwenden Sie WireMock . Fügen Sie die folgenden Abhängigkeiten hinzu:

    <dependency>
        <groupId>com.github.tomakehurst</groupId>
        <artifactId>wiremock</artifactId>
        <version>2.4.1</version>
    </dependency>
    <dependency>
        <groupId>org.igniterealtime.smack</groupId>
        <artifactId>smack-core</artifactId>
        <version>4.0.6</version>
    </dependency>

Definieren und verwenden Sie den Wiremock wie unten gezeigt

@Rule
public WireMockRule wireMockRule = new WireMockRule(8089);

String response ="Hello world";
StubMapping responseValid = stubFor(get(urlEqualTo(url)).withHeader("Content-Type", equalTo("application/json"))
            .willReturn(aResponse().withStatus(200)
                    .withHeader("Content-Type", "application/json").withBody(response)));
8

Wenn Sie unit Ihren Client testen möchten, mockten Sie die Dienste aus, die die REST -API-Aufrufe ausführen, dh mit mockito -. Ich gehe davon aus, dass Sie einen Dienst haben Diese API-Aufrufe für Sie machen, richtig?

Wenn Sie dagegen die restlichen APIs "mockout" machen möchten, da es eine Art Server gibt, der Ihnen Antworten gibt, die eher in Richtung Integrationstest passen, könnten Sie eines der vielen Frameworks wie ausprobieren. restito , rest-driver oder betamax

7
theadam

Mit Mockito können Sie problemlos eine REST - API in Spring Boot simulieren.

Fügen Sie einen stubbed Controller in Ihren Testbaum ein:

@RestController
public class OtherApiHooks {

    @PostMapping("/v1/something/{myUUID}")
    public ResponseEntity<Void> handlePost(@PathVariable("myUUID") UUID myUUID ) {
        assert (false); // this function is meant to be mocked, not called
        return new ResponseEntity<Void>(HttpStatus.NOT_IMPLEMENTED);
    }
}

Ihr Client muss die API unter localhost aufrufen, wenn Sie Tests ausführen. Dies könnte in src/test/resources/application.properties konfiguriert werden. Wenn der Test RANDOM_PORT verwendet, muss Ihr getesteter Client diesen Wert finden. Dies ist ein bisschen schwierig, aber das Problem wird hier angesprochen: Spring Boot - Wie bekomme ich den laufenden Port

Konfigurieren Sie Ihre Testklasse für die Verwendung einer WebEnvironment (eines laufenden Servers). Nun kann Ihr Test Mockito auf die Standardmethode verwenden und je nach Bedarf ResponseEntity-Objekte zurückgeben:

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class TestsWithMockedRestDependencies {

  @MockBean private OtherApiHooks otherApiHooks;

  @Test public void test1() {
    Mockito.doReturn(new ResponseEntity<Void>(HttpStatus.ACCEPTED))
      .when(otherApiHooks).handlePost(any());
    clientFunctionUnderTest(UUID.randomUUID()); // calls REST API internally
    Mockito.verify(otherApiHooks).handlePost(eq(id));
  }

}

Sie können dies auch für End-to-End-Tests Ihres gesamten Microservice in einer Umgebung mit dem oben erstellten Modell verwenden. Eine Möglichkeit, dies zu tun, besteht darin, TestRestTemplate in Ihre Testklasse einzuspeisen und diese zu verwenden, um die Ihre REST - API anstelle von clientFunctionUnderTest aus dem Beispiel aufzurufen.

@Autowired private TestRestTemplate restTemplate;
@LocalServerPort private int localPort; // you're gonna need this too

Wie das geht

Da OtherApiHooks ein @RestController im Testbaum ist, richtet Spring Boot automatisch den angegebenen REST - Dienst ein, wenn der SpringBootTest.WebEnvironment ausgeführt wird.

Mockito wird hier verwendet, um die Controller-Klasse zu verspotten - nicht den Service insgesamt. Daher gibt es einige serverseitige Verarbeitungsvorgänge, die von Spring Boot verwaltet werden, bevor der Mock getroffen wird. Dies kann beispielsweise die Deserialisierung (und Validierung) der im Beispiel gezeigten Pfad-UUID umfassen.

Soweit ich das beurteilen kann, ist dieser Ansatz robust für parallele Testläufe mit IntelliJ und Maven.

1
nobar

Was Sie suchen, ist die Unterstützung für Clientseitige REST -Tests im Spring MVC Test Framework.

Wenn Sie davon ausgehen, dass Ihre NumberClient Spring's RestTemplate verwendet, ist diese oben genannte Unterstützung der richtige Weg!

Hoffe das hilft,

Sam

1
Sam Brannen

Wenn Sie Ruhe verwenden und das DTO-Muster verwenden, würde ich empfehlen, dass Sie diesem Tutorial folgen. 

0
Manu

Hier ein einfaches Beispiel, wie man eine Controller-Klasse mit Mockito verspottet:

Die Controller-Klasse:

@RestController
@RequestMapping("/users")
public class UsersController {

    @Autowired
    private UserService userService;

    public Page<UserCollectionItemDto> getUsers(Pageable pageable) {
        Page<UserProfile> page = userService.getAllUsers(pageable);
        List<UserCollectionItemDto> items = mapper.asUserCollectionItems(page.getContent());
        return new PageImpl<UserCollectionItemDto>(items, pageable, page.getTotalElements());
    }
}

Konfigurieren Sie die Beans:

@Configuration
public class UserConfig {

    @Bean
    public UsersController usersController() {
        return new UsersController();
    }

    @Bean
    public UserService userService() {
        return Mockito.mock(UserService.class);
    }
}

Die Variable UserCollectionItemDto ist ein einfaches POJO und repräsentiert das, was der API-Consumer an den Server sendet. Das UserProfile ist das Hauptobjekt, das in der Serviceschicht (von der UserService-Klasse) verwendet wird. Dieses Verhalten implementiert auch das DTO-Muster .

Schlagen Sie zum Schluss das erwartete Verhalten auf:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(loader = AnnotationConfigContextLoader.class)
@Import(UserConfig.class)
public class UsersControllerTest {

    @Autowired
    private UsersController usersController;

    @Autowired
    private UserService userService;

    @Test
    public void getAllUsers() {
        initGetAllUsersRules();
        PageRequest pageable = new PageRequest(0, 10);
        Page<UserDto> page = usersController.getUsers(pageable);
        assertTrue(page.getNumberOfElements() == 1);
    }

    private void initGetAllUsersRules() {
        Page<UserProfile> page = initPage();
        when(userService.getAllUsers(any(Pageable.class))).thenReturn(page);
    }

    private Page<UserProfile> initPage() {
        PageRequest pageRequest = new PageRequest(0, 10);
        PageImpl<UserProfile> page = new PageImpl<>(getUsersList(), pageRequest, 1);
        return page;
    }

    private List<UserProfile> getUsersList() {
        UserProfile userProfile = new UserProfile();
        List<UserProfile> userProfiles = new ArrayList<>();
        userProfiles.add(userProfile);
        return userProfiles;
    }
}

Die Idee ist, den reinen Controller-Bean zu verwenden und seine Mitglieder nachzubilden. In diesem Beispiel wurde das Objekt UserService.getUsers() über einen Benutzer verspottet und anschließend überprüft, ob der Controller die richtige Anzahl von Benutzern zurückgibt.

Mit derselben Logik können Sie den Dienst und andere Ebenen Ihrer Anwendung testen. In diesem Beispiel wird auch das Controller-Service-Repository-Muster verwendet :)