Jak psát testy na REST API

Jak psát testy na REST API

Jak spát hezké REST API, tak aby bylo dobře testovatelné a jak pak psát testy na takové API.

07ac3a80e69a6252140feb81b89cbb08?s=128

Filip Procházka

October 20, 2018
Tweet

Transcript

  1. 2.

    Co si povíme • Jak vypadá naše api (Java +

    Spring) • Out-of-Container vs End-to-End • Design first? • Spring MVC Test Framework • Spring REST Docs • Jaké testy psát
  2. 3.

    Jak vypadá naše api (Java + Spring) Abych mohl testovat,

    musím mít dobře testovatelné api
  3. 4.

    Jak vypadá naše api (Java + Spring): Testovatelné • Typovost

    ◦ Value Objecty ◦ Mapování na VO vs na Entity ◦ IN -> Scalar -> VO ◦ OUT -> VO • Striktnost ◦ Přijímat pouze jeden jasně definovaný formát ▪ Email není lowercase => 400 ▪ Špatný formát data => 400 • Automatizace
  4. 5.

    Jak vypadá naše api (Java + Spring) • Spring MVC

    ◦ Custom argument resolvers ◦ Custom error handling ▪ Obecná struktura na chyby • Hibernate Validator • Jackson
  5. 6.
  6. 7.

    @PostMapping("/users/{userId}/product-groups") public ResponseEntity<WrappedProductGroupResponse> createProductGroup( final AccessToken accessToken, final Pageable pageable,

    final ClientInfo clientInfo, @PathVariable @AssertUuid final String userId, @RequestBody @Valid final CreateProductGroupRequest createRequest )
  7. 8.

    public static final class CreateProductGroupRequest { @NotNull @NotBlank private String

    name; @AssertEnumValue(value = CustomerSubscriptionSystemModuleSerializable.class) private String module; @AssertEnumValue(value = ProductGroupTypeSerializable.class) private String type; @Valid private CreateProductGroupRequest.ParentRequest parent; @Valid private CreateProductGroupRequest.TemplateRequest template;
  8. 10.

    ProductGroupResult productGroup = productGroupFacade.createProductGroup( accessToken, UUID.fromString(userId), createRequest.getName(), createRequest.toProductGroupType(), createRequest.toSystemModule(), createRequest.toParentId(),

    createRequest.toTemplateId() ); return new ResponseEntity<>( new WrappedProductGroupResponse( productGroupResponseFactory.getProductGroup(productGroup) ), HttpStatus.CREATED );
  9. 11.

    public static final class ProductGroupResponse { private UUID id; private

    String name; private ProductGroupTypeSerializable type; private CustomerSubscriptionSystemModuleSerializable module; private ParentResponse parent; // ... public static final class ParentResponse { private UUID id; private String name; // ... } }
  10. 12.

    try { // … } catch (AccessTokenDoesNotHaveAccessToUserException | UserNotFoundException e)

    { throw userResponseFactory.buildUserNotFound(e); } catch (ProductGroupParentNotFoundException e) { throw productGroupResponseFactory.buildProductGroupParentNotFound(e); } catch (ProductGroupTemplateNotFoundException e) { throw productGroupResponseFactory.buildProductGroupTemplateNotFound(e); } catch (CustomerDoesNotHaveSubscriptionForSystemModuleException e) { throw productGroupResponseFactory .buildCustomerDoesNotHaveSubscriptionForSystemModule(e); }
  11. 13.

    public UnprocessableEntityException buildCustomerDoesNotHaveSubscriptionForSystemModule( final CustomerDoesNotHaveSubscriptionForSystemModuleException e ) { Set<CustomerSubscriptionSystemModuleSerializable> availableModules

    = e.getAvailableModules().stream() .map(CustomerSubscriptionSystemModuleSerializable::fromSystemModule) .collect(Collectors.toSet()); return UnprocessableEntityException.builder() .withCode(ErrorCode.PRODUCT_GROUP_CUSTOMER_DOES_NOT_HAVE_SUBSCRIPTION_FOR_SYSTEM_MODULE) .withTitle("Customer does not have subscription for the system module") .withDetails(String.format( "Available modules are: %s", availableModules.stream() .map(CustomerSubscriptionSystemModuleSerializable::getValue) .collect(Collectors.joining(", ")) )) .addMeta("availableModules", availableModules) .build(e); }
  12. 14.

    public enum ErrorCode { CUSTOMER_LOCKED("customer-locked"), INTERNAL_SERVER_ERROR("internal-server-error"), PRODUCT_NOT_FOUND("product-not-found"), USER_NOT_FOUND("user-not-found"); private final

    String code; ErrorCode(final String code) { this.code = code; } @JsonValue public String getCode() { return code; } @Override public String toString() { return code; } }
  13. 15.

    { "errors" : [ { "code" : "request-validation-error", "title" :

    "Invalid Request", "details" : "Unrecognizable query parameter", "source" : { "parameter" : "filter-wtf" } }, { "code" : "request-validation-error", "title" : "Invalid Request", "details" : "Parameter must be greater than or equal to 1", "source" : { "parameter" : "perPage" } }, { "code" : "request-validation-error", "title" : "Invalid Request", "details" : "Field must not be blank", "source" : { "pointer" : "/password" } } ] }
  14. 17.

    Out-of-Container vs End-to-End • Whitebox vs Blackbox • Asserts ◦

    Internal state of the API • Mocking services ◦ Locking ◦ Faking responses from 3rd party ▪ Payment gateway API
  15. 19.

    Design first • Tools ◦ Blueprint, OpenAPI, ... ◦ Dredd

    • Contract testing ◦ Není náhrada za integrační testy • “Tužka a papír” ◦ Načrtnu strukturu happy path ◦ Vypíšu seznam (bussines) chybových stavů
  16. 22.

    Spring MVC Tests (v podání Cogvio) • Start PostgreSQL +

    ElasticSearch v Docker Containeru • Schema • Fixtures ◦ Naplní PostgreSQL ◦ Naplní ElasticSearch • Snapshot databáze ◦ CREATE DATABASE … WITH TEMPLATE …
  17. 23.

    CreateProductGroupRequest createRequest = new CreateProductGroupRequest( "Foo", // name null, //

    module ProductGroupTypeSerializable.PRODUCT_LIST, null, // parent null // template ); performRequest( post("/v1/users/{userId}/product-groups", userId) .content(toJson(createRequest)) .with(accessToken(UserFixtures.bfuPepaAccessToken))) .andExpect(status().isCreated()) .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8)) .andExpect(jsonPath("$.productGroup.name").value("Foo")) .andExpect(jsonPath("$.productGroup.productsCount").value(0)) .andExpect(jsonPath("$.productGroup.parent").isEmpty());
  18. 24.

    Mockito.doThrow(new ProductGroupLockedException(groupId, userId, new PersistenceException("mock") )) .when(productGroupRepository) .getProductGroupForUpdate( ArgumentMatchers.any(UUID.class), ArgumentMatchers.any(UUID.class)

    ); performRequest( put("/v1/users/{userId}/product-groups/{groupId}", userId, groupId) .content(toJson(new UpdateProductGroupRequest("Maxitrol"))) .with(accessToken(UserFixtures.bfuPepaAccessToken))) .andExpect(status().isConflict()) .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8));
  19. 26.

    Spring REST Docs • Spring MVC Test Framework • Nahrává

    z testů ◦ Reálnou komunikaci ◦ Dokumentaci ◦ Validační annotace • Renderuje
  20. 27.

    http-request.adoc [source,http,options="nowrap"] ---- POST /v1/users/4482f319-dd3a-4a08-9d35-279c90041f6e/product-groups HTTP/1.1 Content-Type: application/json;charset=UTF-8 Accept: application/json

    Authorization: Bearer d4b084e5-66d3-40ff-b2b4-1dd2a8cdccd0 { "name" : "Foo", "module" : null, "type" : "group", "parent" : null, "template" : null } ----
  21. 28.

    http-response.adoc [source,http,options="nowrap"] ---- HTTP/1.1 201 Created Content-Type: application/json;charset=UTF-8 Content-Length: 537

    { "productGroup" : { "id" : "b511a54a-c7fe-4c5e-931e-0c7c80e3586c", "name" : "Foo", "type" : "group", "module" : null, "parent" : null, "productsCount" : 0 } } ----
  22. 30.

    path-parameters.adoc .Path parameters * `userId`: `9b4917db-0b88-4954-8e52-43c7e96c24ab` (`UUID`) - Id of

    the relevant user * `groupId`: `44a0decf-bd9c-4bd1-bc87-b7a5f1272d3d` (`UUID`) - Id of the requested products group
  23. 31.

    index.adoc === GET /v1/users/{userId}/product-groups Outputs all the product groups of

    given user. include::{snippets}/product-group-controller-test/list-product-groups/1/path-parameters.adoc[] include::{snippets}/product-group-controller-test/list-product-groups/1/request-parameters.adoc[] ==== 200 Successful fetch include::{snippets}/product-group-controller-test/list-product-groups/1/http-request.adoc[] *Response* include::{snippets}/product-group-controller-test/list-product-groups/1/http-response.adoc[]
  24. 32.
  25. 33.

    document(requestFields( payloadField("name", String.class, "Name of the product group", describeFieldConstraints(CreateProductGroupRequest.class, "name")),

    payloadField("type", String.class, "Type of the group", describeFieldConstraints(CreateProductGroupRequest.class, "type")), payloadField("module", String.class, "System module for the group", describeFieldConstraints(CreateProductGroupRequest.class, "module")), payloadField("parent", Object.class, "Parent group", describeFieldConstraints(CreateProductGroupRequest.class, "parent")), payloadField("template", Object.class, "Template group (for copying)", describeFieldConstraints(CreateProductGroupRequest.class, "template")), payloadField("template.id", UUID.class, "Id of template group", describeFieldConstraints(CreateProductGroupRequest.ParentRequest.class, "id")) ));
  26. 34.

    request-fields.adoc .Request body fields |=== |Path|Type|Description|Constraints |`name` |String |Name of

    the product group |Must not be blank. Must not be null. |`type` |String |Type of the group |Value must be one of [`product-list`, `group`].
  27. 35.

    index.adoc === POST /v1/users/{userId}/product-groups Creates a new product group. include::{snippets}/product-group-controller-test/create-product-group/1/path-parameters.adoc[]

    include::{snippets}/product-group-controller-test/create-product-group/1/request-fields.adoc[] ==== 201 Successful create include::{snippets}/product-group-controller-test/create-product-group/1/http-request.adoc[] *Response* include::{snippets}/product-group-controller-test/create-product-group/1/http-response.adoc[]
  28. 36.