Slide 1

Slide 1 text

Björn Andersson Mocking your codebase without cursing it @[email protected] @gaqzi.bsky.social ✉ [email protected]

Slide 2

Slide 2 text

Mocking your codebase without cursing it Why? • I care about writing code, getting it into production, and iterating for years • Fast and useful tests are what allow you to adapt to change • Mocks are rarely used in a way that let them shine

Slide 3

Slide 3 text

What are mocks?

Slide 4

Slide 4 text

What are mocks? De fi nitions are you mocking me? • Many di ff erent names for the same thing • Fake, mock, stub, and test double are some but most people say mock • A mock is a stand-in replacement for a real object, used for testing • At their simplest, they're a function that always return the same value • At their most complex, they have logic that responds to your inputs to create complex interactions and scenarios to recreate bugs 
 and unique situations, usually controlled with a framework

Slide 5

Slide 5 text

What are mocks? Example function type CatalogFetcher func(productCode string) (catalog.Product, error) func(productCode string) (catalog.Product, error) { if productCode != "banana" { t.Fatal("called with unknown product code: " + productCode) } return catalog.Product{Code: "banana"}, nil }

Slide 6

Slide 6 text

What are mocks? Example stretchr/testify/mock type OffersFetcher interface { Fetch(productCodes ...string) (catalog.Product, error) } type mockOffersFetcher struct { mock.Mock } func (m *mockOffersFetcher) Fetch(productCodes ...string) (catalog.Product, error) { args := m.Called(productCodes) return args.Get(0).(catalog.Product), args.Error(1) }

Slide 7

Slide 7 text

What are mocks? Example stretchr/testify/mock type OffersFetcher interface { Fetch(productCodes ...string) (catalog.Product, error) } type mockOffersFetcher struct { mock.Mock } func (m *mockOffersFetcher) Fetch(productCodes ...string) (catalog.Product, error) { args := m.Called(productCodes) return args.Get(0).(catalog.Product), args.Error(1) }

Slide 8

Slide 8 text

func TestMock(t *testing.T) { offersFetcher := new(mockOffersFetcher) offersFetcher.Test(t) // <-- makes your code not panic! offersFetcher.On("Fetch", []string{"banana"}).Return(catalog.Product{}, nil) } What are mocks? Example stretchr/testify/mock type OffersFetcher interface { Fetch(productCodes ...string) (catalog.Product, error) } type mockOffersFetcher struct { mock.Mock } func (m *mockOffersFetcher) Fetch(productCodes ...string) (catalog.Product, error) { args := m.Called(productCodes) return args.Get(0).(catalog.Product), args.Error(1) }

Slide 9

Slide 9 text

How to use mocks

Slide 10

Slide 10 text

How to use mocks History • Mocks were created to help you design your applications • They allow you to declare your interfaces without writing the code and see how they interact • This creates a focus on what they're doing rather than how they're doing it • Our applications are spending a lot of time pulling data together so we can use them, and if we handle that work intentionally, we can build them simpler

Slide 11

Slide 11 text

How to use mocks Example extracting learnings from incident reports Incident Reports (DB) Formatted for LLM (RAG) Query LLM ProblemFixer.Find("postgres high CPU")

Slide 12

Slide 12 text

How to use mocks Example extracting learnings from incident reports Incident Reports Formatted for LLM Query LLM ProblemFixer.Find("postgres high CPU") Coordinator Collaborator Collaborator Collaborator Incident Reports (DB) Formatted for LLM (RAG)

Slide 13

Slide 13 text

How to use mocks Characteristics • Coordinator is the object we're testing and the one doing the work • Collaborators are the objects the coordinator is working with • Passing data in, using their return values, and handling their errors • If coordinator is doing anything else then it's mixing business logic and collaboration, and that's when mock go bad 👃If your coordinator has more than 2 tests per collaborator, then it's mixing concerns, expect one for happy path and one for errors 👃If your coordinator is collaborating with more than 3-4 objects

Slide 14

Slide 14 text

How mocks go wrong

Slide 15

Slide 15 text

How mocks go wrong De fi nitions business logic & and collaboration • Business logic is ideally a pure function • You pass in all the data to your method or function and it returns a result • These are super easy to write tests for since all the data is there, no need for databases, caches, or even internet access • When collaborating you collect all the data and pass it to the business logic • The coordinator doesn't know if a collaborator in turn is coordinating or doing business logic; it just passes data in and uses the results • Because you only collaborate with mocks, these also run o ff l ine

Slide 16

Slide 16 text

How mocks go wrong Example 1 code func (r *CommentReport) Create(comments []github.Comment, out io.Writer) error { comments = r.filter(comments) if err := r.report(out, comments); err != nil { return fmt.Errorf("failed to create report: %w", err) } return nil }

Slide 17

Slide 17 text

How mocks go wrong Example 1 implementation func (r *CommentReport) Create(comments []github.Comment, out io.Writer) error { comments = r.filter(comments) if err := r.report(out, comments); err != nil { return fmt.Errorf("failed to create report: %w", err) } return nil }

Slide 18

Slide 18 text

How mocks go wrong Example 1 test func TestSurveyReport(t *testing.T) { var called bool reportOutput := func(w io.Writer, cs []github.Comment) error { called = true require.Equal(t, comments(comment(author("World"))), cs, "expected the comments from the filter") require.NotNilf(t, w, "expected a non-nil writer to have been passed in") return nil } report := reporting.NewCommentsReport(reporting.FilterOnlySurveyComments, reportOutput) err := report.Create( comments(comment(author("Hello")), comment(author("World"))), bytes.NewBuffer(nil), ) require.NoError(t, err) require.True(t, called) }

Slide 19

Slide 19 text

How mocks go wrong Example 1 test func TestSurveyReport(t *testing.T) { var called bool reportOutput := func(w io.Writer, cs []github.Comment) error { called = true require.Equal(t, comments(comment(author("World"))), cs, "expected the comments from the filter") require.NotNilf(t, w, "expected a non-nil writer to have been passed in") return nil } report := reporting.NewCommentsReport(reporting.FilterOnlySurveyComments, reportOutput) err := report.Create( comments(comment(author("Hello")), comment(author("World"))), bytes.NewBuffer(nil), ) require.NoError(t, err) require.True(t, called) }

Slide 20

Slide 20 text

How mocks go wrong Example 1 test func TestSurveyReport(t *testing.T) { var called bool reportOutput := func(w io.Writer, cs []github.Comment) error { called = true require.Equal(t, comments(comment(author("World"))), cs, "expected the comments from the filter") require.NotNilf(t, w, "expected a non-nil writer to have been passed in") return nil } report := reporting.NewCommentsReport(reporting.FilterOnlySurveyComments, reportOutput) err := report.Create( comments(comment(author("Hello")), comment(author("World"))), bytes.NewBuffer(nil), ) require.NoError(t, err) require.True(t, called) }

Slide 21

Slide 21 text

How mocks go wrong Example 1 test func TestSurveyReport(t *testing.T) { var called bool reportOutput := func(w io.Writer, cs []github.Comment) error { called = true require.Equal(t, comments(comment(author("World"))), cs, "expected the comments from the filter") require.NotNilf(t, w, "expected a non-nil writer to have been passed in") return nil } report := reporting.NewCommentsReport(reporting.FilterOnlySurveyComments, reportOutput) err := report.Create( comments(comment(author("Hello")), comment(author("World"))), bytes.NewBuffer(nil), ) require.NoError(t, err) require.True(t, called) } 👃 Mixing mocks and real objects

Slide 22

Slide 22 text

How mocks go wrong Example 1 test func TestSurveyReport(t *testing.T) { var called bool filter := func(cs []github.Comment) []github.Comment { return comments(comment(author("World"))) } reportOutput := func(w io.Writer, cs []github.Comment) error { called = true require.Equal(t, comments(comment(author("World"))), cs, "expected the comments from the filter") require.NotNilf(t, w, "expected a non-nil writer to have been passed in") return nil } report := reporting.NewCommentsReport(filter, reportOutput) err := report.Create( comments(comment(author("Hello")), comment(author("World"))), bytes.NewBuffer(nil), ) require.NoError(t, err) require.True(t, called) }

Slide 23

Slide 23 text

How mocks go wrong Example 2 implementation func (r *CommentReport) Create(comments []github.Comment, out io.Writer) error { var keptComments []github.Comment for _, c := range comments { if c.Author != "World" { continue } keptComments = append(keptComments, c) } if err := r.report(out, comments); err != nil { return fmt.Errorf("failed to create report: %w", err) } return nil } 👃 If-statement that isn't error handling

Slide 24

Slide 24 text

How mocks go wrong Example 3 test func TestService_Calculate(t *testing.T) { catalogFetcher := new(mockCatalogFetcher) offersFetcher := new(mockOffersFetcher) s := cart.NewService( catalog.NewService(catalogFetcher), offers.NewService(offersFetcher), ) result, err := s.Calculate([]cart.Item{}) require.NoError(t, err) require.Equal(t, result, &cart.Result{ Valid: true, TotalAmount: 0, TotalTaxAmount: 0, LineItems: nil, }) }

Slide 25

Slide 25 text

How mocks go wrong Example 3 test func TestService_Calculate(t *testing.T) { catalogFetcher := new(mockCatalogFetcher) offersFetcher := new(mockOffersFetcher) s := cart.NewService( catalog.NewService(catalogFetcher), offers.NewService(offersFetcher), ) result, err := s.Calculate([]cart.Item{}) require.NoError(t, err) require.Equal(t, result, &cart.Result{ Valid: true, TotalAmount: 0, TotalTaxAmount: 0, LineItems: nil, }) } 👃 Always mock your direct collaborators

Slide 26

Slide 26 text

How mocks go wrong Example 4 implementation func (s *Service) Save(ctx context.Context, review Review) (Review, error) { if err := validate.Struct(ctx, review); err != nil { return review, fmt.Errorf("failed to validate review: %w", err) } review.updateTimestamps() review, err := s.reviewStore.Save(ctx, review) if err != nil { return Review{}, fmt.Errorf("failed to save review in storage: %w", err) } return review, nil }

Slide 27

Slide 27 text

How mocks go wrong Example 4 implementation func (s *Service) Save(ctx context.Context, review Review) (Review, error) { if err := validate.Struct(ctx, review); err != nil { return review, fmt.Errorf("failed to validate review: %w", err) } review.updateTimestamps() review, err := s.reviewStore.Save(ctx, review) if err != nil { return Review{}, fmt.Errorf("failed to save review in storage: %w", err) } return review, nil }

Slide 28

Slide 28 text

How mocks go wrong Example 4 implementation func (s *Service) Save(ctx context.Context, review Review) (Review, error) { if err := validate.Struct(ctx, review); err != nil { return review, fmt.Errorf("failed to validate review: %w", err) } review.updateTimestamps() review, err := s.reviewStore.Save(ctx, review) if err != nil { return Review{}, fmt.Errorf("failed to save review in storage: %w", err) } return review, nil } 👃 Collaborating with global objects 👃 Collaborating with passed in objects

Slide 29

Slide 29 text

Concerns with mocks

Slide 30

Slide 30 text

Concerns with mocks This will lead to overmocking • When have you mocked too much? • Avoid it by only using mocks for testing collaboration 
 and never mix real and mocked objects when collaborating

Slide 31

Slide 31 text

Concerns with mocks This will lead to a lot of small objects • Yes! And I think it's great • Imagine the worst codebase you've worked on. What would happen if you strictly followed my suggestions today? • These practices are for longterm maintenance of your code and sometimes you have to ignore them for speed, but benchmark and do it when needed

Slide 32

Slide 32 text

Concerns with mocks But the mocks don't test the logic • True, they're testing collaboration, which is also important work • But since you don't use real objects, certain bugs can occur!

Slide 33

Slide 33 text

Concerns with mocks But the mocks don't test the logic func DiscountBeforeTax(amount, discount, taxRate float64) float64 { return (amount*(1.0-discount))*taxRate } func TestCalculator(t *testing.T) { mockDiscountBeforeTax := func(amount, taxRate, discount float64) float64 { return 1 } calc := cart.NewCalculator(mockDiscountBeforeTax) total := calc.Calculate(cart.Item{Quantity: 2, Amount: 1, TaxRate: 1.25, Discount: 0.2}) }

Slide 34

Slide 34 text

Concerns with mocks But the mocks don't test the logic what can we do? • Is our design helping us? • Instead of having a function, should it be a smarter object responsible for the calculation?

Slide 35

Slide 35 text

Types of tests

Slide 36

Slide 36 text

Types of tests Unit tests the fast in-memory ones • Solitary unit tests • Business logic / only works on the data that are passed into it • These tests try to ensure your logic is correct • Sociable unit tests • Coordinates collaboration between objects • This makes sure you pass data around correctly and handle errors

Slide 37

Slide 37 text

Types of tests Acceptance/service/black box integration running application • These tests run your entire application locally, and you test like a user would • Using a web browser or calling the API over the network • Slower than unit tests • Uses real databases, caches, etc. • Often provide networked mocks of third parties • You make these by looking at the request/response of a previous interaction with the third party • These tests exist to verify your service is still wired together correctly • And to make sure you haven't broken an integration that used to work

Slide 38

Slide 38 text

Types of tests End to end/UI tests everything is real and it's sloooow • Uses your application like a user would • Often run in a pre-production environment, but could also be production • All dependencies are real and the goal is to fi nd if anyone has broken

Slide 39

Slide 39 text

Types of tests The test pyramid

Slide 40

Slide 40 text

Conclusions

Slide 41

Slide 41 text

Conclusions Constraints • Only allow your objects to do business logic or collaboration; don't mix them • Only collaborate with 3-4 objects at a time • At four or more, you usually have some mixed concerns that can be combined into one object for more clarity • Your coordinator's tests should only test how data fl ows between 
 the collaborators and their potential errors. • That means you only need two tests per collaborator, 
 one for the happy path and one for errors

Slide 42

Slide 42 text

Conclusions Smells or what to look out for in code reviews 👃if-statements in coordinators that isn't if err != nil 👃Not mocking all collaborators, whether they're functions or objects 👃Collaborating with passed in objects, they're an implied collaborator. Name and pass in 👃Collaborating with global objects, they're an implied collaborator and 
 brings logic • Logging and metrics… yeah, I admit I do use those as globals. I'm all 
 ears for suggestions to avoiding them as global 👃More than ~a handful of tests for your object? Probably doing too much

Slide 43

Slide 43 text

The Zen of Python, Tim Peters […] Special cases aren't special enough to break the rules. Although practicality beats purity. […]

Slide 44

Slide 44 text

Final words

Slide 45

Slide 45 text

Thank you! @[email protected] @gaqzi.bsky.social ✉ [email protected]

Slide 46

Slide 46 text

Resources • Please Don't Mock Me by Justin Searls • Justin gives much more detailed explanation of why it's worth being this strict with how you use mocks. He is a fantastic presenter on testing and his back catalog is worth watching • Growing Object-Oriented Software Guided by Tests by Steve Freeman and Nat Pryce • This book teaches you how to grow your applications test- fi rst and outside-in using mocks to help you. • How to design a test suite you'll love to maintain by Björn Andersson • My sister talk about how I work with tests overall to make them maintainable • Example of a builder to con fi gure complex mocks. Because working with more complex cases can get quite wordy and this is a pattern to name the setup and make it reusable. • The Practical Test Pyramid by Ham Vocke, from Martin Fowler's website • A detailed look at di ff erent type of tests and how they work together • All the little things by Sandi Metz • A refactoring of the Gilded Rose kata into many small objects and why that is great