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.

Avatar for Filip Procházka

Filip Procházka

October 20, 2018
Tweet

More Decks by Filip Procházka

Other Decks in Technology

Transcript

  1. 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. Jak vypadá naše api (Java + Spring) Abych mohl testovat,

    musím mít dobře testovatelné api
  3. 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. Jak vypadá naše api (Java + Spring) • Spring MVC

    ◦ Custom argument resolvers ◦ Custom error handling ▪ Obecná struktura na chyby • Hibernate Validator • Jackson
  5. @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 )
  6. 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;
  7. 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 );
  8. 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; // ... } }
  9. 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); }
  10. 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); }
  11. 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; } }
  12. { "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" } } ] }
  13. 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
  14. 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ů
  15. 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 …
  16. 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());
  17. 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));
  18. Spring REST Docs • Spring MVC Test Framework • Nahrává

    z testů ◦ Reálnou komunikaci ◦ Dokumentaci ◦ Validační annotace • Renderuje
  19. 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 } ----
  20. 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 } } ----
  21. 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
  22. 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[]
  23. 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")) ));
  24. 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`].
  25. 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[]