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

Testing Absolute Units

pelshoff
January 19, 2023

Testing Absolute Units

Isn't writing tests hard? As much as we believe in testing, tests make our life difficult. Too much boilerplate, too rigid, tee dee dee is not for me. Don't you wonder sometimes: what am I even writing tests for?

Worry not! Fear no more! So do I! Fortunately there are tricks that can make our life easy. In fact, with the right approach, tests are the only thing you have to think about... Wouldn't you love to just chill like that?

Let's discuss the pain of development without and with tests. Let's deal with testing Absolute Units.

pelshoff

January 19, 2023
Tweet

More Decks by pelshoff

Other Decks in Programming

Transcript

  1. @pelshoff
    Welcome
    Testing absolute units

    View Slide

  2. @pelshoff
    @pelshoff
    Photo by Moises Alex on Unsplash

    View Slide

  3. @pelshoff
    @pelshoff
    Photo by Stephen Andrews on Unsplash

    View Slide

  4. @pelshoff
    @pelshoff
    Photo by Stephen Andrews on Unsplash
    Uncertainty
    Distrust
    Opaqueness
    High heart rate

    View Slide

  5. @pelshoff
    @pelshoff
    Consistency
    Confidence
    Transparency
    Low heart rate
    Photo by Stephen Andrews on Unsplash

    View Slide

  6. @pelshoff
    Knowing
    Is the key

    View Slide

  7. @pelshoff
    @pelshoff
    Photo by Daniel Mingook Kim on Unsplash

    View Slide

  8. @pelshoff
    @pelshoff
    Learning
    “In software development learning
    is not a big part of the job. It is the
    job.” – Woody Zuill
    https://twitter.com/WoodyZuill/status/1502366061890473985
    Photo by Daniel Mingook Kim on Unsplash

    View Slide

  9. @pelshoff
    @pelshoff
    Confidence
    “Testing is all about confidence.
    When I'm working, I need to be
    confident that I haven't broken
    anything, and that the code that I
    just wrote works as expected. When
    I'm working, that's ALL I need to test
    for.” – Allen Holub
    https://twitter.com/allenholub/status/1538561251868954624
    Photo by Daniel Mingook Kim on Unsplash

    View Slide

  10. @pelshoff
    @pelshoff
    Consider...
    What is there to do?
    What is left to do?
    How should the system behave?
    How should the component behave?
    How should the component internals work?
    Photo by Daniel Mingook Kim on Unsplash

    View Slide

  11. @pelshoff
    @pelshoff
    Create...
    Features
    Feature
    Scenario
    Decomposition
    Implementation
    Photo by Daniel Mingook Kim on Unsplash

    View Slide

  12. @pelshoff
    @pelshoff
    Transform
    … to natural language spec
    … to executable spec
    … to integration test
    … to unit test
    … to code
    Photo by Daniel Mingook Kim on Unsplash

    View Slide

  13. @pelshoff
    @pelshoff
    Purity
    Photo by Daniel Mingook Kim on Unsplash / Joke by XKCD on https://xkcd.com/435/

    View Slide

  14. @pelshoff
    @pelshoff
    Natural language spec
    User stories, story map, example map…
    Photo by Daniel Mingook Kim on Unsplash

    View Slide

  15. @pelshoff
    Executable spec
    Agree with three “it works, for me”

    View Slide

  16. @pelshoff
    @pelshoff
    /tests/features/Policy/Adjustment/nonSchemeMta.feature
    Feature: non-scheme mid-term adjustments
    In order to match my insurance policy to my evolved situation
    As an insured party
    I want to adjust my policy mid-term
    Scenario: Annualised (full term) mta
    Given a non-scheme policy
    And the start date is "2019-01-01"
    And the premium is 365 GBP
    And the policy is bound
    And the policy is opened
    When the effective date is "2019-07-01"
    And the mta is 375 GBP annualised
    Then delta gross premium should be 5.04 GBP
    Scenario: Pro rated mta
    ...
    Photo by Osman Rana on Unsplash

    View Slide

  17. @pelshoff
    Insurance
    We sell it

    View Slide

  18. @pelshoff
    @pelshoff
    What is a non-scheme mid
    term adjustment?
    Photo by Osman Rana on Unsplash
    ● Scheme: We sell an insurance product on behalf of an insurance company (underwriter)
    ● Non-scheme: We sell an insurance product the insurance company sells (broker)
    ● Policy: A contract between a customer and us that said customer is covered for certain perils
    pertaining exposures for certain periods of time
    ● Term: One of a certain period of time, usually either a month or a year
    ● Mid term adjustment: an adjustment while a term is ongoing
    ● Effective date: The date this “version” if the policy goes live
    ● Premium: The fee a customer pays...
    ● Term premium: for all transactions combined
    ● Current premium: for the latest transaction of a would be term
    ● Delta premium: for the latest transaction..?

    View Slide

  19. @pelshoff
    @pelshoff
    What is a non-scheme mid
    term adjustment?
    Photo by Osman Rana on Unsplash
    ● Non-scheme: We don’t decide the premium
    ● Mid term adjustment: for this adjustment to the contract
    ● Premium: and insurance companies might give us current ór delta
    ● Current premium: where current is the price for a full term (and for the next term upon renewal)
    ● Delta premium: and delta is ehm

    View Slide

  20. @pelshoff
    @pelshoff
    What is a delta premium?
    Photo by Osman Rana on Unsplash
    ● You pay 365GBP for a one year policy
    ● You change to a 375GBP premium half-way through
    ● The delta is NOT 10GBP
    ● You’ve paid 365 / ~2 too much
    ● You’ve paid 375 / ~2 too little
    ● 375 / ~2 - 365 / ~2 = ?
    ● ~187.5 - ~182.5 = 5.04

    View Slide

  21. @pelshoff
    @pelshoff
    /tests/features/Policy/Adjustment/nonSchemeMta.feature
    Feature: non-scheme mid-term adjustments
    In order to match my insurance policy to my evolved situation
    As an insured party
    I want to adjust my policy mid-term
    Scenario: Annualised (full term) mta
    Given a non-scheme policy
    And the start date is "2019-01-01"
    And the premium is 365 GBP
    And the policy is bound
    And the policy is opened
    When the effective date is "2019-07-01"
    And the mta is 375 GBP annualised
    Then delta gross premium should be 5.04 GBP
    Photo by Osman Rana on Unsplash

    View Slide

  22. @pelshoff
    @pelshoff
    .../nonSchemeMta.feature | .../NonSchemeMtaContext.php
    And the start date is "2019-01-01"
    Photo by Osman Rana on Unsplash
    /**
    * @Given /^the start date is
    "2019\-01\-01"$/
    */
    public function theStartDateIs
    ($arg1)
    {
    throw new PendingException()
    ;
    }
    /**
    * @Given the start date is :aDate
    */
    public function theStartDateIs
    ($aDate)
    {
    throw new PendingException()
    ;
    }

    View Slide

  23. @pelshoff
    @pelshoff
    behat.yml
    default:
    suites:
    Policy:
    contexts:
    - TestingAbsoluteUnits\Tests\Features\Policy\Adjustment\NonSchemeMtaContext
    paths:
    - ./tests/Features/Policy
    Photo by Osman Rana on Unsplash

    View Slide

  24. @pelshoff
    @pelshoff
    .../nonSchemeMta.feature | .../NonSchemeMtaContext.php
    And the start date is "2019-01-01"
    Photo by Osman Rana on Unsplash
    /**
    * @Given the start date is :aDate
    */
    public function theStartDateIs
    ($aDate)
    {
    $this->policy->updateStartDate
    (
    LocalDate::
    parse(
    trim(
    $aDate, '"')
    )
    );
    }

    View Slide

  25. @pelshoff
    @pelshoff
    .../nonSchemeMta.feature | .../NonSchemeMtaContext.php
    Then delta gross premium should be
    5.04 GBP
    Photo by Osman Rana on Unsplash
    **
    * @Then delta gross premium should be
    :amount :currency
    */
    public function deltaGrossPremiumShouldBe
    (
    $amount,
    $currency
    ){
    $this->policy->assertRecorded
    (
    new PremiumTransacted(
    Money::
    GBP(5_04)
    )
    );
    }

    View Slide

  26. @pelshoff
    @pelshoff
    Terminal
    $ vendor/bin/behat
    Feature: non-scheme mid-term adjustments
    In order to match my insurance policy to my evolved situation
    As an insured party
    I want to adjust my policy mid-term
    Scenario: Annualised (full term) mta
    Given a non-scheme policy
    TODO: write pending definition
    And the start date is "2019-01-01"
    And the premium is 365 GBP
    And the policy is bound
    And the policy is opened
    When the effective date is "2019-07-01"
    And the mta is 375 GBP annualised
    Then delta gross premium should be 5.04 GBP
    1 scenario (1 pending)
    8 steps (1 pending, 7 skipped)
    Photo by Osman Rana on Unsplash

    View Slide

  27. @pelshoff
    @pelshoff
    Terminal
    $ vendor/bin/behat
    1 scenario (1 failed)
    8 steps (7 passed, 1 failed)
    Photo by Osman Rana on Unsplash

    View Slide

  28. @pelshoff
    @pelshoff
    Terminal
    $ vendor/bin/behat
    1 scenario (1 passed)
    8 steps (8 passed)
    Photo by Osman Rana on Unsplash

    View Slide

  29. @pelshoff
    I’m done!
    and I can prove it
    Photo by Osman Rana on Unsplash

    View Slide

  30. @pelshoff
    @pelshoff
    /tests/Features/Policy/Adjustment/NonSchemeMtaTest.php
    Photo by Osman Rana on Unsplash
    /**
    * In order to match my insurance policy to my evolved situation
    * As an insured party
    * I want to adjust my policy mid-term
    */
    public function it_calculates_non_scheme_mta_annualised
    (): void
    {
    Policy::fake()
    ->givenNonSchemePolicy
    ()
    ->givenStartDateIs
    (2019, 1, 1)
    ->givenPremiumIs
    (365, 'GBP')
    ->givenPolicyIsBound
    ()
    ->givenPolicyIsOpened
    ()
    ->whenEffectiveDateIs
    (2019, 7, 1)
    ->whenMtaIs(375, 'GBP', 'annualised')
    ->thenDeltaGrossPremiumShouldBe
    (5.04, 'GBP');
    }

    View Slide

  31. @pelshoff
    @pelshoff
    Heuristics
    Don’t put too many (unimportant) details in feature tests
    Keep the tests high-level
    Photo by Osman Rana on Unsplash

    View Slide

  32. @pelshoff
    Design
    Driven mad by tests

    View Slide

  33. @pelshoff
    @pelshoff
    Top 5 challenges with tests
    5. Scope mix-up
    4. Mocks
    3. Too much, inconsistent or non-deterministic setup
    2. No tests
    1. Other devs
    Photo by Matt Cramblett on Unsplash

    View Slide

  34. @pelshoff
    @pelshoff
    Top 5 challenges with tests
    5. Scope mix-up
    4. Mocks
    3. Too much, inconsistent or non-deterministic setup
    2. No tests
    1. Other devs Design of the “Unit”
    Photo by Matt Cramblett on Unsplash

    View Slide

  35. @pelshoff
    Unit
    of decomposition
    Photo by Matt Cramblett on Unsplash

    View Slide

  36. @pelshoff
    @pelshoff
    class PremiumCalculation
    {
    public function act(PremiumCalculationAction $action): static
    {
    $action->assertActionCanBeApplied
    ($this->transactions, $this->period);
    return $this->withTransaction
    ($action->transact($this->transactions, $this->period));
    }
    public function correct(PremiumCalculationCorrection $correction): static
    {
    $this->transactions->assertThereAreTransactions
    ();
    $correction->assertCorrectionCanBeApplied
    ($this->transactions, $this->period);
    $correctedCalculation = $this->withTransaction
    (
    LastTransactionPremiumReversal::
    for($this->transactions)
    );
    return $correctedCalculation
    ->withTransaction
    (
    CorrectionTransaction::
    fromTransaction(
    $correction->transact($correctedCalculation
    ->transactions, $this->period)
    )
    );
    }
    }
    src/Premium/PremiumCalculation.php
    Photo by Matt Cramblett on Unsplash

    View Slide

  37. @pelshoff
    @pelshoff
    /** @test */
    public function can_adjust()
    {
    $bindPremium = new SinglePremium(Money::
    GBP(365_00), ...);
    $adjustPremium = new SinglePremium(Money::
    GBP(375_00), ...);
    $deltaPremium = new SinglePremium(Money::
    GBP(5_04), ...);
    $expected = Transactions::
    of([
    new PremiumTransaction(LocalDate::
    of(2019, 1, 1), $bindPremium, $bindPremium),
    new PremiumTransaction(LocalDate::
    of(2019, 7, 1), $adjustPremium
    , $deltaPremium),
    ]);
    $actual = Transactions::
    of(
    PremiumCalculation::
    for(TermPeriod::of(...))
    ->act(new CurrentPremiumBind(
    $bindPremium))
    ->act(new CurrentPremiumAdjustment(LocalDate::
    of(2019, 7, 1), $adjustPremium
    ))
    ->newTransactions
    ()
    );
    $this->assertEquals($expected, $actual);
    }
    tests/Premium/PremiumCalculationTest.php
    Photo by Matt Cramblett on Unsplash

    View Slide

  38. @pelshoff
    What’s special
    about this?
    Photo by Matt Cramblett on Unsplash

    View Slide

  39. @pelshoff
    @pelshoff
    class PremiumCalculationTest extends TestCase
    {
    public function is_immutable(){}
    public function wont_bind_when_there_are_already_transactions
    (){}
    public function can_bind(){}
    public function needs_existing_transactions_before_adjusting
    (){}
    public function can_adjust(){}
    public function can_bind_then_adjust_multiple_times
    (){}
    public function can_correct_a_bind
    (){}
    public function wont_correct_with_a_new_bind_if_an_adjustment_happened
    (){}
    public function can_correct_an_adjustment
    (){}
    }
    tests/Premium/...
    Photo by Matt Cramblett on Unsplash

    View Slide

  40. @pelshoff
    @pelshoff
    class PremiumCalculationTest extends TestCase
    {
    public function is_immutable(){}
    public function can_bind_then_adjust_multiple_times
    (){}
    public function can_correct_a_bind
    (){}
    public function wont_correct_with_a_new_bind_if_an_adjustment_happened
    (){}
    public function can_correct_an_adjustment
    (){}
    }
    class CurrentPremiumAdjustmentTest extends TestCase
    {
    public function needs_existing_transactions
    (){}
    public function can_only_append_transactions
    (){}
    public function must_be_within_term_bounds
    (){}
    public function can_adjust_to_a_higher_current_premium
    (){}
    public function can_adjust_to_a_lower_current_premium
    (){}
    public function can_adjust_on_the_first_day
    (){}
    public function can_adjust_on_the_final_day
    (){}
    }
    class TransactionsTest extends TestCase
    {
    public function can_determine_term_premium
    (){}
    }
    tests/Premium/...
    Photo by Matt Cramblett on Unsplash

    View Slide

  41. @pelshoff
    @pelshoff
    $ vendor/bin/phpunit --testdox
    Premium Calculation (Tests\...\PremiumCalculation)
    ✔ Is immutable
    ✔ Can bind then adjust multiple times
    ✔ Can correct a bind
    ✔ Wont correct with a new bind if an adjustment happened
    ✔ Can correct an adjustment
    Current Premium Adjustment (Tests\...\CurrentPremiumAdjustment)
    ✔ Needs existing transactions
    ✔ Can only append transactions
    ✔ Must be within term bounds
    ✔ Can adjust to a higher current premium
    ✔ Can adjust to a lower current premium
    ✔ Can adjust on the first day
    ✔ Can adjust on the final day
    Transactions (Tests\...\Transactions)
    ✔ Can determine term premium
    Terminal
    Photo by Matt Cramblett on Unsplash

    View Slide

  42. @pelshoff
    @pelshoff
    1. Fix the feature test
    2. Fix the integration test
    3. Fix the unit test
    4. Fix the integration test
    5. Fix the feature test
    1. Make it work
    2. Make it beautiful
    3. (Make it fast)
    Executable to-do list
    Photo by Matt Cramblett on Unsplash

    View Slide

  43. @pelshoff
    @pelshoff
    /** @test */
    public function can_adjust()
    {
    $bindPremium = new SinglePremium(Money::
    GBP(365_00), ...);
    $adjustPremium = new SinglePremium(Money::
    GBP(375_00), ...);
    $deltaPremium = new SinglePremium(Money::
    GBP(5_04), ...);
    $expected = Transactions::
    of([
    new PremiumTransaction(LocalDate::
    of(2019, 1, 1), $bindPremium, $bindPremium),
    new PremiumTransaction(LocalDate::
    of(2019, 7, 1), $adjustPremium
    , $deltaPremium),
    ]);
    $actual = Transactions::
    of(
    PremiumCalculation::
    for(TermPeriod::of(...))
    ->act(new CurrentPremiumBind(
    $bindPremium))
    ->act(new CurrentPremiumAdjustment(LocalDate::
    of(2019, 7, 1), $adjustPremium
    ))
    ->newTransactions
    ()
    );
    $this->assertEquals($expected, $actual);
    }
    tests/Premium/PremiumCalculationTest.php
    Photo by Matt Cramblett on Unsplash

    View Slide

  44. @pelshoff
    @pelshoff
    Executable to-do list
    Error : Class "TestingAbsoluteUnits\Tests\Unit\PremiumCalculation" not found
    Error : Call to undefined method TestingAbsoluteUnits\Premium\PremiumCalculation::forTerm()
    Error : Call to undefined method TestingAbsoluteUnits\Premium\PremiumCalculation::act(), etc.
    TypeError : TestingAbsoluteUnits\Premium\PremiumCalculation::act(): Argument #1 ($param) must be of type
    TestingAbsoluteUnits\Tests\Unit\CurrentPremiumAdjustment,
    TestingAbsoluteUnits\Premium\Adjustment\CurrentPremiumAdjustment given
    PremiumCalculation::newTransactions(): Return value must be of type array, null returned
    Failed asserting that two objects are equal.
    Photo by Matt Cramblett on Unsplash

    View Slide

  45. @pelshoff
    @pelshoff
    /** @test */
    public function can_adjust()
    {
    $bindPremium = new SinglePremium(Money::
    GBP(365_00), ...);
    $adjustPremium = new SinglePremium(Money::
    GBP(375_00), ...);
    $deltaPremium = new SinglePremium(Money::
    GBP(5_04), ...);
    $expected = Transactions::
    of([
    new PremiumTransaction(LocalDate::
    of(2019, 1, 1), $bindPremium, $bindPremium),
    new PremiumTransaction(LocalDate::
    of(2019, 7, 1), $adjustPremium
    , $deltaPremium),
    ]);
    $actual = Transactions::
    of(
    PremiumCalculation::
    for(TermPeriod::of(...))
    ->act(new CurrentPremiumBind(
    $bindPremium))
    ->act(new CurrentPremiumAdjustment(LocalDate::
    of(2019, 7, 1), $adjustPremium
    ))
    ->newTransactions
    ()
    );
    $this->assertEquals($expected, $actual);
    }
    tests/Premium/PremiumCalculationTest.php
    Photo by Matt Cramblett on Unsplash

    View Slide

  46. @pelshoff
    @pelshoff
    Executable to-do list
    [] vs. [0 => PremiumTransaction Object (), 1 => PremiumTransaction Object ()]
    [...] vs. [..., 1 => PremiumTransaction Object ()]
    Error : Call to undefined method TestingAbsoluteUnits\Premium\Adjustment\CurrentPremiumAdjustment::transact()
    [..., ...37500...] vs. [..., ...504...]
    The form of the results is now the same. What’s next?
    Photo by Matt Cramblett on Unsplash

    View Slide

  47. @pelshoff
    What’s hard
    about this?
    Photo by Matt Cramblett on Unsplash

    View Slide

  48. @pelshoff
    @pelshoff
    /** @test */
    public function can_adjust_to_a_higher_current_premium
    ()
    {
    $bindPremium = new SinglePremium(Money::
    GBP(365_00), ...);
    $adjustPremium = new SinglePremium(Money::
    GBP(375_00), ...);
    $deltaPremium = new SinglePremium(Money::
    GBP(5_04), ...);
    $bindDate = LocalDate::of(2019, 1, 1);
    $adjustDate = LocalDate::of(2019, 7, 1);
    $adjustment = new CurrentPremiumAdjustment(
    $adjustDate, $adjustPremium
    );
    $expected = new PremiumTransaction(
    $adjustDate, $adjustPremium
    , $deltaPremium);
    $actual = $adjustment->transact(
    Transactions::
    fromBind($bindDate, $bindPremium),
    TermPeriod::fullYearOf($adjustDate)
    );
    $this->assertEquals($expected, $actual);
    }
    tests/Premium/Calculation/Adjustment/CurrentPremiumAdjustmentTest.php
    Photo by Matt Cramblett on Unsplash

    View Slide

  49. @pelshoff
    @pelshoff
    public function transact(Transactions $transactions, TermPeriod $period): Transaction
    {
    // Given the number of days we have left
    // Take what we've paid so far
    // Subtract what we've paid too much for the old policy
    // Add what we pay for the new policy
    $effectiveNumberOfDaysForNewCurrentPremium = $this->effectiveDate->
    daysUntil
    ($period->exclusiveEndDate
    ());
    $deltaPremium = $transactions->termPremium()
    ->multiply($period->fullTermDays())
    ->subtract(
    $transactions
    ->currentPremium
    ()
    ->
    multiply($effectiveNumberOfDaysForNewCurrentPremium
    )
    )
    ->add($this->premium->multiply($effectiveNumberOfDaysForNewCurrentPremium
    ))
    ->divide($period->fullTermDays())
    ->subtract($transactions->termPremium());
    return new PremiumTransaction(
    $this->effectiveDate, $this->premium, $deltaPremium);
    }
    src/Premium/Calculation/Adjustment/CurrentPremiumAdjustment.php
    Photo by Matt Cramblett on Unsplash

    View Slide

  50. @pelshoff
    @pelshoff
    Heuristics
    Photo by Matt Cramblett on Unsplash
    A unit is a component you can test in isolation; Write tests with isolation and no mocks to create units
    Fight the urge to automate and/or abstract test set-up; if it’s hard make it smaller
    Large units are hard to TDD; if it’s hard make it smaller
    Keep your suite fast and Run The Tests
    Use test failures as a to-do list
    Highlight what’s special
    Highlight what’s hard

    View Slide

  51. @pelshoff
    Test quality
    Is your code writing checks that your tests can’t
    cache?

    View Slide

  52. @pelshoff
    @pelshoff
    sum.php | sumTest.php
    function sum(int $x, int $y) {
    return $x + $y;
    }
    $this->assertEquals(4, sum(2, 2));
    Photo by Maksym Kaharlytskyi on Unsplash

    View Slide

  53. @pelshoff
    @pelshoff
    divide.php | divideTest.php
    function divide(int $x, int $y): float
    {
    return $x / $y;
    }
    $this->assertEquals(2, divide(4, 2));
    Photo by Maksym Kaharlytskyi on Unsplash

    View Slide

  54. @pelshoff
    @pelshoff
    divide.php | divideTest.php
    function divide(int $x, int $y): float
    {
    if ($y === 0) {
    throw new DivisorFail();
    }
    return $x / $y;
    }
    $this->expectException
    (
    DivisorFail::
    class
    );
    divide(1, 0);
    Photo by Maksym Kaharlytskyi on Unsplash

    View Slide

  55. @pelshoff
    @pelshoff
    Transactions.php | TransactionsTest.php
    public function sum(): int
    {
    if (count($this->transactions) <= 0) {
    return 0;
    }
    return array_reduce(
    $this->transactions,
    fn(int $sum, int $transaction) =>
    $sum + $transaction,
    0
    );
    }
    public function it_sums_to_zero
    (): void
    {
    $transactions = new Transactions([])
    ;
    $this->assertEquals(
    0,
    $transactions
    ->sum()
    );
    }
    public function it_sums_good(): void
    {
    $transactions = new Transactions(
    [3,4,5]
    );
    $this->assertEquals(
    12,
    $transactions
    ->sum()
    );
    }
    Photo by Maksym Kaharlytskyi on Unsplash

    View Slide

  56. @pelshoff
    @pelshoff
    Terminal
    $ XDEBUG_MODE=coverage vendor/bin/infection
    .: killed, M: escaped, U: uncovered, E: fatal error, X: syntax error, T: timed out, S: skipped, I:
    ignored
    M........ (9 / 9)
    9 mutations were generated:
    8 mutants were killed
    0 mutants were configured to be ignored
    0 mutants were not covered by tests
    1 covered mutants were not detected
    0 errors were encountered
    0 syntax errors were encountered
    0 time outs were encountered
    0 mutants required more time than configured
    Metrics:
    Mutation Score Indicator (MSI): 88%
    Mutation Code Coverage: 100%
    Covered Code MSI: 88%
    Please note that some mutants will inevitably be harmless (i.e. false positives).
    Photo by Maksym Kaharlytskyi on Unsplash

    View Slide

  57. @pelshoff
    @pelshoff
    infection.log
    Escaped mutants:
    ================
    1) Transactions.php:17 [M] LessThanOrEqualTo
    --- Original
    +++ New
    @@ @@
    }
    public function sum() : int
    {
    - if (count($this->transactions) <= 0) {
    + if (count($this->transactions) < 0) {
    return 0;
    }
    return array_reduce($this->transactions, fn(int $sum, int $transaction) => $sum +
    $transaction, 0);
    }
    }
    Photo by Maksym Kaharlytskyi on Unsplash

    View Slide

  58. @pelshoff
    @pelshoff
    Transactions.php | TransactionsTest.php
    public function sum(): int
    {
    if (count($this->transactions) <= 0) {
    return 0;
    }
    return array_reduce(
    $this->transactions,
    fn(int $sum, int $transaction) =>
    $sum + $transaction,
    0
    );
    }
    public function it_sums_to_zero
    (): void
    {
    $transactions = new Transactions([])
    ;
    $this->assertEquals(
    0,
    $transactions
    ->sum()
    );
    }
    public function it_sums_good(): void
    {
    $transactions = new Transactions(
    [3,4,5]
    );
    $this->assertEquals(
    12,
    $transactions
    ->sum()
    );
    }
    Photo by Maksym Kaharlytskyi on Unsplash

    View Slide

  59. @pelshoff
    @pelshoff
    infection.json
    {
    "$schema": "vendor/infection/infection/resources/schema.json"
    ,
    "testFrameworkOptions": "--testsuite=Infection",
    "source": {
    "directories": [
    "app"
    ]
    },
    "mutators": {
    "@default": true,
    "@cast": false
    },
    "logs": {
    "text": "infection.log"
    }
    }
    $ XDEBUG_MODE=coverage vendor/bin/infection --only-covered
    Photo by Maksym Kaharlytskyi on Unsplash

    View Slide

  60. @pelshoff
    Production code
    Tests
    Mutations

    View Slide

  61. @pelshoff
    @pelshoff
    Infection
    Mutation testing lib for PHP
    Analyses source tree and runs your suite for each applied mutation
    Prefer regular PHPUnit TestCase for performance reasons
    Run on entire suite or on changed files only
    © Markus Schirp (https://twitter.com/_m_b_j_)
    Photo by Maksym Kaharlytskyi on Unsplash

    View Slide

  62. @pelshoff
    @pelshoff
    Infection
    On my M1 Macbook
    86 test cases
    Infection running with --only-covered for 128 mutants
    \PHPUnit\TestCase: 0.057s
    Infection: 14s
    \Illuminate\Foundation\Testing\TestCase: 4.712s
    Infection: 48s
    Photo by Maksym Kaharlytskyi on Unsplash

    View Slide

  63. @pelshoff
    @pelshoff
    divide.php | divideTest.php
    function divide(int $x, int $y): float
    {
    return $x / $y;
    }
    $this->assertEquals(2, divide(4, 2));
    Photo by Maksym Kaharlytskyi on Unsplash

    View Slide

  64. @pelshoff
    @pelshoff
    divide.php | divideTest.php
    function divide(int $x, int $y): float
    {
    return $x / $y;
    }
    use Eris\Generators
    ;
    use Eris\TestTrait
    ;
    $this->forAll(
    Generators::
    int(),
    Generators::
    int()
    )
    ->then(fn(int $x, int $y) =>
    $this->assertIsNumeric
    (
    divide(
    $x, $y))
    );
    OutOfBoundsException : Evaluation ratio
    0.01 is under the threshold 0.5
    Caused by
    DivisionByZeroError: Division by zero
    Reproduce with:
    ERIS_SEED=1646663840606414
    vendor/bin/phpunit --filter '...'
    Photo by Maksym Kaharlytskyi on Unsplash

    View Slide

  65. @pelshoff
    @pelshoff
    Eris
    Property testing lib for PHP
    “Brute forces” towards edge cases
    Prefer regular PHPUnit TestCase for performance reasons
    Photo by Maksym Kaharlytskyi on Unsplash

    View Slide

  66. @pelshoff
    @pelshoff
    Photo by Stephen Andrews on Unsplash

    View Slide

  67. @pelshoff
    |u|

    View Slide

  68. @pelshoff

    View Slide

  69. @pelshoff
    Pim Elshoff
    twitter.com/pelshoff
    [email protected]
    speakerdeck.com/pelshoff
    Testing Absolute Units

    View Slide