Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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.

Filip Procházka

October 20, 2018
Tweet

More Decks by Filip Procházka

Other Decks in Technology

Transcript

  1. Jak psát testy
    na REST API
    @ProchazkaFilip

    View Slide

  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

    View Slide

  3. Jak vypadá naše api (Java + Spring)
    Abych mohl testovat, musím mít dobře testovatelné api

    View Slide

  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

    View Slide

  5. Jak vypadá naše api (Java + Spring)
    ● Spring MVC
    ○ Custom argument resolvers
    ○ Custom error handling
    ■ Obecná struktura na chyby
    ● Hibernate Validator
    ● Jackson

    View Slide

  6. View Slide

  7. @PostMapping("/users/{userId}/product-groups")
    public ResponseEntity createProductGroup(
    final AccessToken accessToken,
    final Pageable pageable,
    final ClientInfo clientInfo,
    @PathVariable
    @AssertUuid
    final String userId,
    @RequestBody
    @Valid
    final CreateProductGroupRequest createRequest
    )

    View Slide

  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;

    View Slide

  9. public ProductGroupType toProductGroupType()
    {
    return Optional.ofNullable(type)
    .map(ProductGroupTypeSerializable::forValue)
    .map(ProductGroupTypeSerializable::toGroupType)
    .orElse(null);
    }

    View Slide

  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
    );

    View Slide

  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;
    // ...
    }
    }

    View Slide

  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);
    }

    View Slide

  13. public UnprocessableEntityException buildCustomerDoesNotHaveSubscriptionForSystemModule(
    final CustomerDoesNotHaveSubscriptionForSystemModuleException e
    )
    {
    Set 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);
    }

    View Slide

  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; }
    }

    View Slide

  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"
    }
    } ]
    }

    View Slide

  16. Out-of-Container vs End-to-End
    (MockServer vs Full HTTP server)

    View Slide

  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

    View Slide

  18. Design first?

    View Slide

  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ů

    View Slide

  20. Spring MVC Tests

    View Slide

  21. Spring MVC Test Framework
    ● Integruje Spring s testovacími FW
    ● Zkratky, utility, …

    View Slide

  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 …

    View Slide

  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());

    View Slide

  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));

    View Slide

  25. Spring REST Docs

    View Slide

  26. Spring REST Docs
    ● Spring MVC Test Framework
    ● Nahrává z testů
    ○ Reálnou komunikaci
    ○ Dokumentaci
    ○ Validační annotace
    ● Renderuje

    View Slide

  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
    }
    ----

    View Slide

  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
    }
    }
    ----

    View Slide

  29. document(pathParameters(
    guidParameter("userId", "Id of the relevant user"),
    parameterWithName(ProductGroupController.FILTER_PARENT_GROUP_PARAM)
    .description("Search in the relevant parent product group")
    ));

    View Slide

  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

    View Slide

  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[]

    View Slide

  32. View Slide

  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"))
    ));

    View Slide

  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`].

    View Slide

  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[]

    View Slide

  36. View Slide

  37. Jaké testy psát

    View Slide

  38. Jaké testy psát
    ● Happy path
    ● All the errors
    ● Some edge cases?

    View Slide

  39. @ProchazkaFilip

    View Slide