liveBook · Manning

Functional core

As explained in Unit Testing Principles, Practices, and Patterns

A functional core is the part of an architecture that makes decisions through computations with explicit inputs and outputs and minimal side effects.

A functional core makes business decisions from explicit inputs and returns explicit values, while minimizing side effects and mutable state. A mutable shell gathers inputs and applies the core’s decisions to external systems. section 6.3 section 6.6

Keep the boundary between the core and shell deliberate:

  • Put parsing, preparation, branching, and business rules in the core. section 6.4
  • Represent decisions with values containing enough information for the shell to act without making further decisions. section 6.3
  • Test the core through outputs and cover the shell with fewer integration tests. section 6.3 section 6.4
  • Treat collaborators such as databases as hidden inputs; pass their products into the core instead. section 6.5

Refactoring an audit system

An initial AuditManager reads and writes the filesystem, forcing tests to work with files. Injecting an IFileSystem enables mocking, but leaves setup and verification moderately maintainable. Refactoring AuditManager to accept FileContent values and return a FileUpdate moves all decisions into the core. Persister reads files and applies updates; ApplicationService connects the two. A test can now supply full file contents and assert that an overflowing audit file produces audit_3.txt with Alice’s record. section 6.4 section 6.5

Listing 6.15. The test without mocks
[Fact]
public void A_new_file_is_created_when_the_current_file_overflows()
{
    var sut = new AuditManager(3);
    var files = new FileContent[]
    {
        new FileContent("audit_1.txt", new string[0]),
        new FileContent("audit_2.txt", new string[]
        {
            "Peter; 2019-04-06T16:30:00",
            "Jane; 2019-04-06T16:40:00",
            "Jack; 2019-04-06T17:00:00"
        })
    };

    FileUpdate update = sut.AddRecord(
        files, "Alice", DateTime.Parse("2019-04-06T18:00:00"));

    Assert.Equal("audit_3.txt", update.FileName);
    Assert.Equal("Alice;2019-04-06T18:00:00", update.NewContent);
}

Use the output-based audit test as a compact model of supplying values to the core and asserting its returned decision. section 6.4

How Unit Testing Principles, Practices, and Patterns covers it

How deep it goes
Explains explicit values, mutable shells, refactoring steps, testing boundaries, and performance and code-size trade-offs.
Examples
Uses discount and increment functions, an audit-file system, FileContent and FileUpdate values, Persister, and ApplicationService.
What it assumes
Assumes familiarity with side effects, immutable and mutable objects, collaborators, and unit-test quality attributes.
What you can do after
Build it from scratch by moving decisions into pure computations, returning explicit updates, and leaving external effects to a thin shell.
How it connects
Connects functional cores with mutable shells, immutability, encapsulation, output-based testing, and hexagonal architecture.

Related concepts

This explainer was generated by Manning's knowledge graph from the text of Unit Testing Principles, Practices, and Patterns. The section links point to the parts of the book it draws on.