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

JCON 2023 - Your Tests also need some Architecting

Avatar for Jonas Geiregat Jonas Geiregat
June 20, 2023
130

JCON 2023 - Your Tests also need some Architecting

Avatar for Jonas Geiregat

Jonas Geiregat

June 20, 2023

Transcript

  1. The reality of tests - Brittle - Complex - Hard

    to maintain - Did not guide - Cryptic - Slow - Skipped
  2. You do not only write a test in the current

    moment for the current moment
  3. - Requirements - Integrations - Security - Technology - Data

    Formats - Deployments - People - Organizations We write test to cope with change
  4. Other things are also important but if your software can

    change you can always: make it faster make it more secure make it more resilient make it more user-friendly
  5. Engineers love solving problems, that have zero business value because

    it’s considered fun. What do we write code for ?
  6. “The trigger for writing a test is not a new

    class nor a new public method” -- Ian Cooper - TDD, Where Did It All Go Wrong
  7. The main reason for a test to exists is because

    there is a requirement that describes a behavior of the system
  8. So you want us to only write integration tests ?

    -- Fellow developers - coffee machine
  9. unit tests end to end tests integration tests slow /

    expensive fast / cheap Does the classical testing Pyramid still stand ?
  10. The type of test: unit, integration, acceptance etc .. is

    how you are testing. It is a technical detail, that is seldom relevant.
  11. integrated impl. detail tests integration behavior / feature / use-case

    tests How vs What end to end tests unit tests integration tests Testing honeycomb Kent C. Dodds pyramid static static
  12. Requirement: An email notification should be sent to the receiver

    once an invoice is paid Defined by the EmailService interface Implemented by the SesEmailService
  13. Test should not be viewed in the same manner one

    views production code. It’s a different context. Different rules are at play.
  14. Keep your readers in your test method Minimize setup and

    tear-down logic to only the technical
  15. Classic test naming convention class InvoiceServiceTest { class SesEmailServiceTest {

    VS Test naming convention based on requirement/feature/behavior class PayInvoiceFeatureTest { class MarkInvoiceAsProcessedFeatureTest { class CreateInvoiceTemplateFeatureTest { class ProcessInvoiceFeatureTest { class AddInvoiceLineFeatureTest {
  16. Do not communicate what you are testing @Test void testExpireInvoice()

    { @Test void testStartReportJob() { @Test void addPineappleToppingToPizzaTest() {
  17. Don’t communicate what you are testing but why you need

    a test The why captures the behaviour
  18. The why @Test void testExpireInvoice() { @Test void unExpiredInvoice_shouldExpire_after30DaysAfterPublicationDate() {

    @Test void testStartReportJob() { @Test void startingANewReportJob_shouldUpdateTheStateToRunning() { @Test void addPineappleToppingToPizzaTest() { @Test void pineappleIsAnIllegalTopping_andShouldPreventTheCookFromAddingIt() {
  19. Address billingAddress = new Address.Builder() .streetAddress("123 Main St") .city("Anytown") .state("CA")

    .zipCode("12345") .country(Country.US) .build(); Address shippingAddress = new Address.Builder() .streetAddress("456 Oak Ave") .city("Othertown") .state("CA") .zipCode("67890") .country(Country.US) .build(); List<InvoiceItem> items = new ArrayList<>(); items.add(new InvoiceItem("Product A", 100.00, Tax.vatPercentage(21))); items.add(new InvoiceItem("Product B", 100.00, Tax.vatPercentage(21))); Customer customer = new Customer.Builder() .name("John Doe") .email("[email protected]") .phoneNumber("555-123-4567") .build(); Invoice invoice = new Invoice.Builder() .invoiceNumber("001") .customer(customer) .billingAddress(billingAddress) .shippingAddress(shippingAddress) .invoiceDate(LocalDate.now()) .items(items) .build(); var amount = invoice.getVatAmount(); assertThat(amount).isEqualTo(Amount.EUR(42))
  20. // given Address billingAddress = new Address.Builder() .streetAddress("123 Main St")

    .city("Anytown") .state("CA") .zipCode("12345") .country(Country.USA) .build(); Address shippingAddress = new Address.Builder() .streetAddress("456 Oak Ave") .city("Othertown") .state("CA") .zipCode("67890") .country(Country.USA) .build(); List<InvoiceItem> items = new ArrayList<>(); items.add(new InvoiceItem("Product A", 100.00, Tax.vatPercentage(21))); items.add(new InvoiceItem("Product B", 100.00, Tax.vatPercentage(21))); Customer customer = new Customer.Builder() .name("John Doe") .email("[email protected]") .phoneNumber("555-123-4567") .build(); Invoice invoice = new Invoice.Builder() .invoiceNumber("001") .customer(customer) .billingAddress(billingAddress) .shippingAddress(shippingAddress) .invoiceDate(LocalDate.now()) .items(items) .build(); // when var amount = invoice.getVatAmount(); // then assertThat(amount).isEqualTo(Amount.EUR(42))
  21. InvoiceMother.validInvoice(); InvoiceMother.validInvoiceWithUSShippingAddress(); InvoiceMother.validInvoiceFromYesterday(); InvoiceMother.validInvoiceWithInvoiceLines(10); InvoiceMother.validInvoice() <InvoiceMother.Builder> .withInvoiceItems( new InvoiceItem("Product A",

    Amount.EUR(100), Tax.vatPercentage(21)), new InvoiceItem("Product B", Amount.EUR(100), Tax.vatPercentage(21))) .build(); <Invoice> Limit your factory methods, override what matters with setters InvoiceMother.validInvoice() .withShippingAddress(address -> address.country(Country.US)) .build() Emphasize what matters and hide the irrelevant parts
  22. //given var invoice = InvoiceMother.validInvoice() .withInvoiceItems( new InvoiceItem("Product A", Amount.EUR(100),

    Tax.vatPercentage(21)), new InvoiceItem("Product B", Amount.EUR(100), Tax.vatPercentage(21))) .build(); // when var amount = invoice.getVatAmount(); // then assertThat(amount).isEqualTo(Amount.EUR(42)) Emphasize what matters and hide the irrelevant details
  23. org.opentest4j.AssertionFailedError: Expecting value to be true but was false Expected

    :true Actual :false assertThat(invoice.isPaid()).isTrue();
  24. org.opentest4j.AssertionFailedError: Expecting invoice to be paid but was not. Expected

    :true Actual :false Assertions.assertThat(actual.isPaid()) .withFailMessage("Expecting invoice to be paid but was not.") .isTrue();
  25. class InvoiceAssertions extends AbstractAssert<InvoiceAssertions, Invoice> { protected InvoiceAssertions(Invoice invoice) {

    super(invoice, InvoiceAssertions.class); } public static InvoiceAssertions assertThat(Invoice invoice) { return new InvoiceAssertions(invoice); } public InvoiceAssertions isPaid() { Assertions.assertThat(actual.isPaid()) .withFailMessage("Expecting invoice to be paid but was not.") .isTrue(); return this; } }
  26. //given InvoiceMother.validInvoice() .withInvoiceItems( new InvoiceItem("Product A", Amount.euro(100.00), Tax.vatPercentage(21)), new InvoiceItem("Product

    B", Amount.euro(100.00), Tax.vatPercentage(21))) .build(); // when var amount = invoice.getVATAmount(); // then assertThat(invoice).hasAVatAmountOf(Amount.euro(42.00))
  27. @Test void shouldShiftFrom_errored_toOpen_onRevive_whenTheExpirationDateIsInTheFuture() { // given var clock = Clock.fixed(Instant.parse("2023-01-05T00:00:00.00Z"),

    ZoneId.of("UTC")); var transaction = TransactionMother.defaultTransaction() .withStatus(ERRORED) .withExpirationDate(LocalDate.of(2024, 5, 1)) .build(); // when var shiftedTransaction = transaction.shift(REVIVE, clock); // then assertThat(shiftedTransaction).hasShiftedTo(OPEN); }
  28. Change is inevitable that is why we test Behavior is

    what we test Accepting that tests live in a different context is how we test
  29. Test and production code should be equal citizens looked at

    from different context with equal architectural rights cared for equally equally important