agentleFS
patfornapatforna/core-skills/skills/goos/SKILL.md

goos

Skill0 starsChanged 5 months agoMITView on GitHub

Outside-in TDD and object design (Freeman & Pryce). Use when designing object interactions, structuring tests for collaborating objects, or wrapping third-party code. TRIGGER when: user asks about outside-in TDD, mock roles, walking skeleton, ports and adapters, or test-driving object design. DO NOT TRIGGER when: pure data transformation code with no object collaboration, or user is doing basic Beck-style TDD (use /core-skills:tdd instead).

The file

Show its rules and text

We split this skill into the rules it gives an agent

36 rules. About 0 are common, 34 are distinctive, and 2 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈34

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 2

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Write a failing acceptance test that exercises the feature end-to-end through the system's external interfaces.

    line 14
  2. Distinctive:

    Leave it failing. It defines "done" for this feature.

    line 15
  3. Distinctive:

    Before any feature work, build a walking skeleton.

    line 25
  4. Distinctive:

    Write one acceptance test that exercises the simplest possible path through the whole system (e.g., "start up, do nothing meaningful, shut …

    line 31
  5. Distinctive:

    Stop. Feature work starts now.

    line 34
  6. Distinctive:

    When an object needs something done, it tells a collaborator to do it.

    line 42
  7. Very specific:

    When you find yourself writing obj.getX() followed by a decision based on X, move the decision into obj.

    line 46
  8. Distinctive:

    Use this classification to decide what to inject, what to mock, and what to allow in tests.

    line 58
  9. Distinctive:

    When writing a unit test for object A that needs to talk to some collaborator:

    line 66
  10. Distinctive:

    Create an interface with exactly that method.

    line 69
  11. Distinctive:

    Do not start from the implementation and work backward to the interface.

    line 73
  12. Distinctive:

    If a class or method name contains "And", "Or", or "But", it has multiple responsibilities.

    line 77
  13. Distinctive:

    When composing objects, the composite's API should be simpler than the sum of its components' APIs.

    line 81
  14. Distinctive:

    If mocking is painful, the design has a problem:

    line 115
  15. Distinctive:

    When your code depends on a third-party library or external system:

    line 127
  16. Distinctive:

    When integrating a third-party library, write tests that verify your assumptions about how it behaves.

    line 138
  17. Distinctive:

    Document your understanding of the library

    line 140
  18. Distinctive:

    Add behavior in thin end-to-end slices.

    line 148
  19. Distinctive:

    Follow the failure inward, writing unit tests for each object that needs to change.

    line 151
  20. Distinctive:

    Never rip the application apart for a feature.

    line 155
  21. Distinctive:

    When test-driving reveals a new collaborator you're not ready to implement:

    line 159
  22. Distinctive:

    Add the real implementation to the to-do list.

    line 163
  23. Distinctive:

    When you notice a new domain concept emerging (scattered primitives, repeated parameter groups, a string being passed around):

    line 168
  24. Very specific:

    When a generic collection (e.g., List[Thing], Dict[str, Thing]) is passed through multiple methods, wrap it in a domain type.

    line 176
  25. Distinctive:

    Test names describe behavior, not implementation.

    line 182
  26. Distinctive:

    Every test has three parts, in this order:

    line 188
  27. Distinctive:

    Setup / Given: Create the object, configure stubs/allowances

    line 190
  28. Distinctive:

    Action / When: Call the method under test

    line 191
  29. Distinctive:

    Assertions / Then: Check expectations and results

    line 192
  30. Distinctive:

    When test objects require complex construction with many fields:

    line 205
  31. Distinctive:

    Create a builder with sensible defaults for every field.

    line 207
  32. Distinctive:

    Use builders when objects have more than 2--3 constructor parameters.

    line 212
  33. Distinctive:

    Test difficulty is the primary design feedback signal.

    line 216
  34. Distinctive:

    Do not use exceptions for control flow in the domain.

    line 235
  35. Distinctive:

    When testing code with asynchronous behavior:

    line 239
  36. Distinctive:

    When you find domain code importing from an external library, extract an adapter.

    line 256

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

GOOS -- Growing Object-Oriented Software, Guided by Tests

Outside-in TDD for systems with collaborating objects. This skill layers on top of the TDD skill (/core-skills:tdd), which covers the inner red/green/refactor cycle. This skill covers the outer loop: acceptance tests, object discovery, protocol design, and architectural boundaries.

The Two Loops

Outer Loop: Acceptance Test

  1. Write a failing acceptance test that exercises the feature end-to-end through the system's external interfaces.
  2. Leave it failing. It defines "done" for this feature.
  3. Work inward using unit tests (inner loop) until the acceptance test passes.
  4. Refactor. Commit.

Inner Loop: Unit Tests

For each object needed to make the acceptance test pass, apply the TDD skill's red/green/refactor cycle. The difference from standalone TDD: each unit test is motivated by the failing acceptance test, and you discover collaborator interfaces as you go.

Walking Skeleton

Before any feature work, build a walking skeleton.

What it is: The thinnest possible end-to-end slice that connects all major architectural components (entry point, domain logic, external boundaries) and passes one trivial acceptance test.

How to build it:

  1. Write one acceptance test that exercises the simplest possible path through the whole system (e.g., "start up, do nothing meaningful, shut down cleanly").
  2. Build just enough to pass it: real entry point, real wiring, stub implementations where needed.
  3. Establish the build, deploy, and test infrastructure so the acceptance test runs automatically.
  4. Stop. Feature work starts now.

When to stop: The skeleton is done when the acceptance test passes and you can build, deploy, and test in one command. It does not need real behavior.

Object Design Rules

Tell, Don't Ask

When an object needs something done, it tells a collaborator to do it. It does not query the collaborator's state and decide what to do.

Violation: if (account.getBalance() > amount) { account.debit(amount); } Fix: account.debit(amount) -- let the account enforce its own rules.

When you find yourself writing obj.getX() followed by a decision based on X, move the decision into obj.

Object Peer Stereotypes

Classify each dependency of an object into one of three roles:

StereotypeRelationshipInject via
DependencyService the object cannot function withoutConstructor
NotificationTold about events; sender doesn't care about receiver's responseConstructor/setter
AdjustmentChanges behavior of the object (policy, strategy, config)Constructor/setter

Use this classification to decide what to inject, what to mock, and what to allow in tests. Dependencies and notifications are the primary mock targets.

Context Independence

An object must not know about the system it runs in. It receives everything it needs through constructor parameters or method arguments. If an object reaches out to a global, a singleton, or a context object to get a collaborator, extract that dependency and inject it.

Interface Discovery

When writing a unit test for object A that needs to talk to some collaborator:

  1. Define what A needs the collaborator to do (the message A wants to send).
  2. Create an interface with exactly that method.
  3. Mock that interface in the test.
  4. The interface name should describe the role (e.g., AuctionHouse, SniperListener), not the implementation.

Do not start from the implementation and work backward to the interface. The caller's needs define the interface.

No And/Or/But

If a class or method name contains "And", "Or", or "But", it has multiple responsibilities. Split it.

Composite Simpler Than Its Parts

When composing objects, the composite's API should be simpler than the sum of its components' APIs. If wiring objects together makes the system harder to understand, the decomposition is wrong.

Narrow Interfaces

Interfaces should have as few methods as possible. A large interface suggests the role is doing too much. Split it into smaller, focused roles. One-method interfaces are often ideal.

Value Types

Wrap related primitives into immutable value objects that represent domain concepts (prices, quantities, identifiers). Two techniques:

  • Breaking out: Extract related fields that are always passed together into a new value type.
  • Bundling up: Group method parameters that belong together into a value type.

Mock Roles, Not Objects

What to Mock

Mock interfaces that represent roles in your design. Never mock:

  • Concrete classes (couples test to implementation)
  • Value objects (use real instances)
  • Third-party types you don't own (wrap them first -- see Adapter Wrapping below)

Expectations Vs. Allowances

In test doubles, distinguish between:

  • Expectations: calls the test asserts must happen. These are the point of the test.
  • Allowances/stubs: calls the test permits but doesn't assert. These are supporting infrastructure to get the object into the right state.

Make this distinction visible in the test. Expectations go in the assertion section. Stubs go in the setup section.

What Mocks Tell You

If mocking is painful, the design has a problem:

Mocking PainDesign ProblemFix
Too many mocks neededObject has too many dependenciesSplit object, introduce intermediate role
Complex mock setupObject has too many responsibilitiesBreak the object apart
Mocking concrete classesMissing interface/protocolExtract interface for the role
Expectations prescribing exact orderImplicit state machineMake state explicit (enum, state object)
Test setup looks nothing like realityAbstraction mismatchRaise or lower the test's abstraction level

Adapter Wrapping (Only Mock Types You Own)

When your code depends on a third-party library or external system:

  1. Define a domain interface that describes what your code needs in your domain's language (e.g., AuctionHouse.auctionFor(itemId), not SmackXMPPConnection.createChat(...)).
  2. Write an adapter that implements your domain interface and delegates to the third-party API.
  3. Unit-test your domain code against the domain interface (mock it).
  4. Integration-test the adapter against the real third-party system.

This applies to: databases, message queues, HTTP clients, UI frameworks, file systems, clocks, and any library whose API you do not control.

Learning Tests

When integrating a third-party library, write tests that verify your assumptions about how it behaves. These tests:

  • Document your understanding of the library
  • Break early when the library changes on upgrade
  • Are not unit tests of your code -- they test the library

Growing an Object Incrementally

Keyhole Surgery

Add behavior in thin end-to-end slices. For each slice:

  1. Extend the acceptance test (or write a new one) to cover the new behavior.
  2. Follow the failure inward, writing unit tests for each object that needs to change.
  3. Get the acceptance test passing.
  4. Refactor.

Never rip the application apart for a feature. Each step should leave the code working.

Defer Decisions

When test-driving reveals a new collaborator you're not ready to implement:

  1. Define the interface.
  2. Provide a null/empty implementation to get the code compiling.
  3. Add the real implementation to the to-do list.
  4. Stay focused on the current test.

Budding Off

When you notice a new domain concept emerging (scattered primitives, repeated parameter groups, a string being passed around):

  1. Introduce a new type as a placeholder -- even just a wrapper with one field.
  2. Push behavior onto it incrementally as the code reveals what belongs there.
  3. Replace primitives with the new type across the codebase.

Encapsulate Collections

When a generic collection (e.g., List[Thing], Dict[str, Thing]) is passed through multiple methods, wrap it in a domain type. The domain type provides a place to attach behavior and eliminates duplication of iteration/filtering logic.

Test Readability

Test Names

Test names describe behavior, not implementation. They read as specifications of what the object does.

Good: reports_lost_if_auction_closes_when_bidding Bad: test_auction_closed_method

Test Structure

Every test has three parts, in this order:

  1. Setup / Given: Create the object, configure stubs/allowances
  2. Action / When: Call the method under test
  3. Assertions / Then: Check expectations and results

Separate these visually (blank lines, comments, or section markers). When using mocks, stubs go in setup; expectations go in assertions.

Helper Methods

Extract repeated setup and assertion patterns into helper methods. Name them in the domain language:

  • Good: a_sniper_that_is(BIDDING), an_item_with_stop_price(1000)
  • Bad: create_mock(state=2), setup_test_data()

Test Data Builders

When test objects require complex construction with many fields:

  1. Create a builder with sensible defaults for every field.
  2. Expose .with_X(value) methods for each field.
  3. Tests override only the fields they care about.
  4. This keeps tests focused on what matters and resilient to constructor changes.

Use builders when objects have more than 2--3 constructor parameters. Use simple factory methods (object mothers) for simpler cases.

Listening to the Tests

Test difficulty is the primary design feedback signal. When a test is hard to write, do not blame the test -- fix the design.

SymptomDiagnosisRefactoring
Long, complicated test setupObject under test has too many responsibilitiesSplit the object. Extract collaborators.
Too many mocksToo many dependenciesIntroduce an intermediate object that bundles related deps.
Hard to construct the objectConstructor does too much or has implicit depsSimplify constructor. Extract hidden deps as explicit params.
Tests are fragile (break on changes)Action at a distance -- tight couplingReduce coupling. Introduce interfaces at boundaries.
Duplicated test setupMissing concept or abstractionExtract the concept into a helper, builder, or new type.
Bloated constructors (many params)Object has too many responsibilities or missing value objectsBundle related params into a value object. Split the object.
Tests prescribe exact call orderObject has implicit stateMake state explicit with a state object or enum.

Notifications Vs. Exceptions

UseWhen
Notification/eventThe outcome is part of normal domain flow, even if it represents "failure" (e.g., auction lost, bid rejected). The sender does not care what the receiver does.
ExceptionThe condition should never happen and indicates a programming error (e.g., impossible state, violated invariant). Use a Defect-style runtime exception. Do not catch it.

Do not use exceptions for control flow in the domain. Do not use notifications for programming errors.

Asynchronous Testing

When testing code with asynchronous behavior:

  1. Never use sleep(). Timing-based tests are flaky.
  2. Use probes/polls: repeatedly check for the expected condition with a timeout.
  3. Prefer sampling over listening: poll the system's observable state rather than registering for internal notifications (which couples the test to implementation).
  4. Synchronize on observable state changes: the test waits for a visible result (UI change, message sent, file written), not for internal state transitions.
  5. Fail with a timeout and a descriptive message that shows what condition was not met.

Ports and Adapters Architecture

The design that emerges from outside-in TDD naturally produces ports and adapters:

  • Domain core: pure business logic, no references to external systems. All collaborators are domain interfaces.
  • Ports: the domain interfaces that the core exposes (inbound) or consumes (outbound).
  • Adapters: implementations of outbound ports that translate to external systems. Inbound adapters translate external events into domain calls.
  • Entry point (Main): wires adapters to domain objects. Does not contain logic. Acts as a "matchmaker."

When you find domain code importing from an external library, extract an adapter.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.