Skip to content

Instantly share code, notes, and snippets.

@duongphuhiep
Last active March 20, 2026 10:15
Show Gist options
  • Select an option

  • Save duongphuhiep/b7ad0edd5db2b41bcdb9095f97e76002 to your computer and use it in GitHub Desktop.

Select an option

Save duongphuhiep/b7ad0edd5db2b41bcdb9095f97e76002 to your computer and use it in GitHub Desktop.
Solitary vs Sociable Unit Test

PART 1 - Solitary vs Sociable Unit Test (The "London School" vs. "Detroit School")

Term Scope Mocks
Unit Test (Solitary) One method / one class All direct dependencies.
Unit Test (Sociable) One feature / multiple classes Only external systems (DB/API).
Integration Test System + external No mocks (or very few).

Example: we want to unit test the PaymentService class which depends on IPaymentExecutor and IPaymentRecorder

  • IPaymentExecutor calls external PaypalApi
  • IPaymentRecorder upload some data to BlobStorage
Unit test (or “Solitary Unit test”) Component test (or “Sociable Unit test“)
GIVEN a IPaymentExecutor and a IPaymentRecorder mocks, WHEN PaymentService is called THEN I assert that the mocks had been called with expected input. GIVEN a Database situation WHEN PaymentService is called THEN I assert that the PaypalApi had been called with expected input and a “record” had been uploaded to the BlobStorageMock with expected content.
IPaymentExecutor and IPaymentRecorder are implementation details of the PaymentService => the unit test changes (or break) when these details implementation changed. Database, PaypalApi, and BlobStorage are external services => The component test changes (or breaks) when the application behavior changes.
The Unit tests reflect what DIRECT dependencies (either internal or external) “see“ the “Unit under test”. The Component tests reflect what external components (the outside world) “see” the application.

Note: In this example, the database is considered as External component. But many Component tests might include the database as Internal component. In this case, the tests spin up real database instead of mocking it. Library such as “Test Container” does the trick.

In modern software engineering, there are two main schools of thought about unit testing:

  • Solitary (London School): Mocks every direct dependency. When people say "unit test", many developers mean a "solitary" unit test.
  • Sociable (Detroit/Chicago School): Uses real classes for everything inside your application boundary, and only mocks the "out-of-process" dependencies (DB, APIs).
    • You can still call it a "Unit Test" under the Detroit School definition, as long as it stays in-memory and runs fast.
    • Or call it a "Component Test": your test covers the entire feature logic, from the Service down to the Repository. You are testing the "component" (your business logic) as a single unit, treating the database and external API as the only "outside" worlds.
    • How about calling it an "Integration Test"? Traditionally, if a test touches more than one class, many developers call it an integration test. However, "integration test" often implies you are testing how your code integrates with the real database or real API. Since you are still mocking those, the terms "component test" or "sociable unit test" are usually more accurate.
Feature Solitary (Mocking Everything) Sociable (Mocking only Externals)
Refactoring High friction
When internal methods change, tests break easily.
Low friction
Tests stay green as long as the result is correct.
Bug Detection Low
Misses bugs in how classes talk to each other.
Low protection against regressions caused by refactoring.
High
Catches integration issues between your classes.
Strong regression prevention during refactoring.
Edge cases Setup is simple.
Can test whatever edge cases (or combinations) you can imagine.
Setup is more complex.
Usually tests only critical paths or realistic edge cases.
Scaling Scales with interactions.
The more interactions between classes change, the more tests need to be added or adapted.
Scales with behavior.
The more behavior changes, the more tests need to be added or adapted.

Many senior developers (notably Martin Fowler and Ian Cooper) recommend sociable unit tests - especially if you have many solitary unit tests to maintain.

  • Your 1000+ solitary unit tests don't give you enough confidence.
  • They feel useless (or low value).
  • Adding more tests feels like the work of a code-generation robot just to hit 100% test coverage in SonarQube.
  • You want to increase confidence before a heavy refactoring.

Then "component tests" (or sociable unit tests) is the answer. That's said Component tests will scare you.

  1. The initial setup for component tests is expensive. It will becomes incremental over time. In the long run, the maintainability cost of component tests and solitary unit tests should be similar.
  2. However maintaining and writing Component test requires developers to master the whole application behavior. Not just a small module or a small piece of codes.
    • If "the input and the database looks like this then the application should call this and this external api endpoint and output this result..." without full understanding the application's behavior down to the small detail, Developers won't be able to write good Component tests.
    • In the other hand, this difficulty make the Component test valuable. Because it will also help us to judge if the application behavior is normal.

Read also:

My recommendation

Follow both schools:

  • For complex functions with a lot of algorithmic/computational logic ⇒ solitary unit tests.
  • For functions with little logic that mostly call other functions ⇒ sociable unit tests.

If you're hesitating, I would prioritize: E2E tests (integration tests) > component tests > (solitary) unit tests.

  • Favor E2E tests as long as the "cost" is reasonable and the tests result is reliable.
    • Note: cost = effort to set up + development + maintenance.
  • If certain scenarios are too "expensive" for E2E tests ⇒ fall back to component tests.
  • If certain scenarios are too "expensive" for component tests ⇒ fall back to (solitary) unit tests.

Note: Component tests often replicate E2E test cases, and that's not redundant—think of them as "cheap and fast E2E tests" that are useful during development.

PART 2 - Writing unit tests

Problem 1

A test method usually has three parts: Arrange, Act, Assert.

You might write 10 test cases that are mostly similar, where each test case has slightly different Arrange or Assert steps. => so you try to create shared Arrange/Assert blocks that can be reused across tests.

Over time, the situation can become spaghetti:

//Used in Test1, Test2
void Arrange1(bool cond) {
  if (cond) {..}
}

//Used in Test2, Test3
fun Arrange2() {
}

//Used in Test1, Test3
fun Assert1() {}

//Used in Test1, Test6
fun Assert2(bool cond) {}
  • Update "Test1"
  • => Need to slightly update "Arrange1" and "Assert2"
  • => It makes "Test2" and "Test6" fail...

My recommendation

Use the Builder pattern: create small reusable Arrange/Assert units instead of one big Arrange/Assert block, so different tests can compose them as needed.

void TestCase() {
	// Arrange database mock
	TestDataGenerator dataGenerator = new(_dbContext);
	dataGenerator.GenerateEntityX();
	dataGenerator.GenerateEntityY();
	dataGenerator.GenerateRowA();
	dataGenerator.GenerateEntityXY();
	dataGenerator.GenerateEntityX2();
	dataGenerator.GenerateRowAB();
	await _dbContext.SaveChangesAsync();
	
	// Arrange SomeApi mock
	SomeApiFakeResponseBuilder SomeApiResponseBuilder = new(_services, _logger);
	SomeApiResponseBuilder
	    .GetEndpoint1_Success()
	    .PostEndpoint2_Success()
	    .PutEndpoint3_InternalError()
	    .GetEndpoint4_NotFound()
		.Build();
}

Think of these Arrange/Assert units as "Atoms", you can compose them to form bigger Assert/Arrange block as "Molecule".

  • The "molecule" must to be scoped in a Single Test Class.
  • The "molecule" should make sense, they should have a clear name. For eg: "Given_Order_With_OutOfStock_Product()".
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment