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

Mocking your codebase without cursing it

Mocking your codebase without cursing it

Working with mocks is a lot like working out: if you don’t know what you’re doing then you’ll either a) don’t get the results you want or b) hurt yourself. And in the worst case, you’ll get both and be unhappy with the results and swear off mocking as a useless practice.

This talk will show some common pitfalls of using mocks (updating a lot of mocks to return the new correct value, mocking out the same complex thing again and again, elaborate setups to exercise a piece of business logic, and more) and how you can constrain how you work with mocks to make them work for you and make your test suite something you’ll love to maintain.

Presented at GopherCon Singapore.
If you find this talk interesting check out How to design a test suite you'll love to maintain which talks about how I design test suites in general and it has a recording.

Resources (links from the last slide):

- 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-first and outside-in using mocks to help you.
- Example of a builder to configure 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.
- How to design a test suite you'll love to maintain by Björn Andersson (me!). My sister talk about how I work with tests overall to make them maintainable, there's a link to a recording there
- The Practical Test Pyramid by Ham Vocke, from Martin Fowler's website. A detailed look at different 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

Björn Andersson

January 24, 2025
Tweet

More Decks by Björn Andersson

Other Decks in Technology

Transcript

  1. 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
  2. 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
  3. 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 }
  4. 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) }
  5. 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) }
  6. 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) }
  7. 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
  8. How to use mocks Example extracting learnings from incident reports

    Incident Reports (DB) Formatted for LLM (RAG) Query LLM ProblemFixer.Find("postgres high CPU")
  9. 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)
  10. 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
  11. 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
  12. 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 }
  13. 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 }
  14. 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) }
  15. 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) }
  16. 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) }
  17. 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
  18. 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) }
  19. 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
  20. 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, }) }
  21. 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
  22. 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 }
  23. 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 }
  24. 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
  25. 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
  26. 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
  27. 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!
  28. 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}) }
  29. 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?
  30. 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
  31. 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
  32. 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
  33. 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
  34. 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
  35. The Zen of Python, Tim Peters […] Special cases aren't

    special enough to break the rules. Although practicality beats purity. […]
  36. 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