Chapter 4. Vertical Slice

Some years ago, a regular client of mine asked me to come help with a project. When I arrived, I learned that a team had been working on a task for about half a year without getting anywhere.

Their task was indeed daunting, but they were stuck in Analysis Paralysis[15]. There were so many requirements that the team couldn’t figure out how to address them all. I’ve seen this happen more than once, with different teams.

Sometimes, the best strategy is to just get started. You should still think and plan ahead. There’s no reason to be wilfully nonchalant or blasé about thinking ahead, but just as too little planning can be bad for you, so can too much. If you’ve already established your deployment pipeline[49], the sooner you can deploy a piece of working software, no matter how trivial, the sooner you can start to collect feedback from stakeholders[29].

Start by creating and deploying a vertical slice of the application.

4.1 Start with working software

How do you know that software works? Ultimately, you don’t know until you’ve shipped it. Once it’s deployed or installed, and being used by real users, you may be able to verify whether or not it works. That’s not even the final assessment. The software that you’ve developed may work as you intended it to work, while not solving users’ actual problems. How to address that problem is beyond the scope of the book, so I’ll leave it at that1. I interpret software engineering as a methodology to make sure that the software works as intended, and that it stays that way.

1 The Lean Startup[87] and Accelerate[29] are good starting points if you need to explore that topic.

The idea behind vertical slicing is to get to working software as soon as possible. You do that by implementing the simplest feature you can think of - all the way from user interface to data store.

4.1.1 From data ingress to data persistence

Most software comes with two types of boundaries to the wider world. You’ve probably seen diagrams similar to figure 4.1 before. Data arrives at the top. The application may sub ject the input to various transformations, and may ultimately decide to save it.

Images

Figure 4.1: Typical architecture diagram. Data arrives at the top, flows through the application (the box), and is persisted at the bottom (in the can).

Even a read operation can be considered input, although it doesn’t result in data being saved. A query typically comes with query parameters that identify the data being requested. The software still transforms those input values to an interaction with its data store.

Sometimes, the data store is a dedicated database. At other times, it’s just another system. It could be an HTTP-based service somewhere on the internet, a message queue, the file system, or even just the local computer’s standard output stream.

Such downstream targets can be write-only systems (e.g. the standard output stream), read-only systems (e.g. a third-part HTTP API), or read-write systems (e.g. the file system or databases).

Thus, at a sufficiently high level of abstraction, the diagram in figure 4.1 describes most software, from web sites to command-line utilities.

4.1.2 Minimal vertical slice

You can organise code in various ways. A conventional architecture is to organise constituent elements into layers[33][26][50][60]. You don’t have to do it like that, but the context of layered application architecture helps explain why it’s called a vertical slice.

You don’t have to organise your code in layers. This section only discusses layered architecture to explain why it’s called a vertical slice.

As figure 4.2 shows, layers are typically illustrated as horizontal strata, with data arriving on top and being persisted at the bottom. In order to implement a complete feature, you’ll have to move data all the way from entry point to persistence layer, or the other way. When layers are horizontal shapes, then a single feature is a vertical slice through all of them.

Images

Figure 4.2: A vertical slice through the horizontal layers of a stereotypical application architecture.

Regardless of whether you organise your code in layers or in another way, implementing an end-to-end feature has at least two benefits:

1. It gives you early feedback about the entire life cycle of your software development process.

2. It’s working software. It might already be useful to a user.

I’ve met programmers who spent months perfecting a home-grown data-access framework before even trying to use it to implement a feature. They often learn that they’ve made assumptions about usage patterns that don’t fit reality. You should avoid Speculative Generality[34], the tendency to add features to code because you ‘might need it later’. Instead, implement features with the simplest possible code, but look out for duplication as you add more.

Implementing a vertical slice is an effective way to learn what sort of code you need, and what you can do without.

4.2 Walking Skeleton

Find a motivation for making changes to the code. Such motivation acts as a driver of the change, so to speak.

You’ve already seen examples of such drivers. When you treat warnings as errors, when you turn on linters and other static code analysers, you introduce extrinsic motivations for changing the code. This can be beneficial because it removes a degree of subjective judgment.

Using drivers of change gives rise to a whole family of x-driven software development methodologies:

1. Test-driven development[9] (TDD)

2. Behaviour-driven development (BDD)

3. Domain-driven design[26] (DDD)

4. Type-driven development

5. Property-driven development2

2 See subsection 15.3.1 for an example of property-based testing.

Recall the bat-and-ball-problem from chapter 3. It demonstrates how easy it is to make mistakes. Using an extrinsic driver works a bit like double-entry bookkeeping[63]. You somehow interact with the driver, and it induces you to modify the code.

While the driver can be a linter, it can also be code, in the form of automated tests. I often follow an outside-in style of test-driven development. This is a technique where the first tests you write exercise the high-level boundary of the system under test. From there, you can work your way from the outside in by adding tests of more fine-grained implementation details, as needed. You’ll find a more detailed explanation, including an example, in section 4.3.

You’re going to need a test suite.

4.2.1 Characterisation Test

In the rest of this chapter, I’m going to show you how to add a vertical slice to the restaurant reservation HTTP API I began developing in subsection 2.2.2. Right now, it only serves the plain text Hello World!.

If you add a simple automated test of the system, you’re on track to enabling test-driven development. You’ll have a thin slice of functionality that you can automatically test and deploy: a Walking Skeleton[36].

Follow the new-code-base checklist introduced in section 2.2 when adding a unit test project to the Visual Studio solution: add the new test project to Git, treat warnings as errors, and make sure that the automated build runs the test.

Once you’ve done that, add your first test case, like listing 4.1.

[Fact]
public async Task HomeIsOk()
{
    using var factory = new WebApplicationFactory<Startup>();
    var client = factory.CreateClient();

    var response = await client
        .GetAsync(new Uri("", UriKind.Relative))
        .ConfigureAwait(false);

    Assert.True(
        response.IsSuccessStatusCode,
        $"Actual status code: {response.StatusCode}.");
}

Listing 4.1: Integration test of the HTTP home resource. (Restaurant/3ee0733/Restaurant.RestApi.Tests/HomeTests.cs)

To be clear, I wrote this test after the fact, so I didn’t follow test-driven development. Rather, this type of test is called a Characterisation Test[27] because it characterises (i.e. describes) the behaviour of existing software.

I did this because the software already exists. You may recall from chapter 2 that I used a wizard to generate the initial code. Right now it works as intended, but how do we know that it’ll keep working?

I find it prudent to add automated tests to protect against regressions.

The test shown in listing 4.1 uses the xUnit.net unit testing framework. This is the framework I’ll use throughout the book. Even if you’re not familiar with it, it should be easy to follow the examples, as it follows well-known patterns for unit testing[66].

It uses the test-specific WebApplicationFactory<T> class to create a self-hosted instance of the HTTP application. The Startup class (shown in listing 2.5) defines and bootstraps the application itself.

Notice that the assertion only considers the most superficial property of the system: does it respond with an HTTP status code in the 200 range (e.g. 200 OK or 201 Created)? I decided to refrain from verifying anything stronger than that, because the current behaviour (it returns Hello World!) only acts as a placeholder. It should change in the future.

When only asserting that a Boolean expression is true, the only message you’ll get from the assertion library is that true was expected, but the actual value was false. That’s hardly illuminating, so it may prove helpful to provide a bit of extra context. I did that here by using the overload to Assert.True that takes an additional message as its second argument.

I find the test too verbose as presented, but it compiles and the test passes. We’ll improve the test code in a moment, but first, keep the new-code-base checklist in mind. Did I do anything that the build script should automate? Yes, indeed, I added a test suite. Change the build script to run the tests. Listing 4.2 shows how I did that.

#!/usr/bin/env bash
dotnet test --configuration Release

Listing 4.2: Build script with tests. (Restaurant/3ee0733/build.sh)

Compared to listing 2.3, the only change is that it calls dotnet test instead of dotnet build.

Remember to follow the checklist. Commit the changes to Git.

4.2.2 Arrange Act Assert

There’s structure to the test in listing 4.1. It starts with two lines followed by a blank line, then a single statement spanning three lines followed by a blank line, and finally another single statement spanning three lines.

Most of that structure is the result of deliberate methodology. For now, I’ll skip the reason that some statements span multiple lines. You can read about that in subsection 7.1.3.

The blank lines, on the other hand, are there because the code follows the Arrange Act Assert pattern[9], also known as the AAA pattern. The idea is to organise a unit test into three phases.

1. In the arrange phase you prepare everything required for the test.

2. In the act phase you invoke the operation you’d like to test.

3. In the assert phase you verify that the actual outcome matches the expected outcome.

You can turn that pattern into a heuristic. I usually indicate the three phases by separating them with a blank line. That’s what I’ve done in listing 4.1. This only works if you can avoid additional blank lines in the test. A common problem is when the arrange section grows so big that you get the urge to apply some formatting by adding blank lines. If you do that, you’ll have more than two blank lines in your test, and it’s unclear which of them delineate the three phases.

In general, consider it a code smell[34] when test code grows too big. I like it best when the three phases balance. The act section is typically the smallest, but if you imagine that you rotate the code 90° as shown in figure 4.3, you should be able to balance the code approximately on the act section.

Images

Figure 4.3: Imagine that you rotate your test code 90°. If you can position it approximately on its act phase, then it’s in balance.

If the test code is so big that you must add additional blank lines, you’ll have to resort to code comments to identify the three phases[91], but try to avoid this.

At the other extreme, you may occasionally write a miniscule test. If you only have three lines of code, and if each line belongs to each of the different AAA phases, you can dispense with the blank lines; similarly if you have only one or two lines of code. The purpose of the AAA pattern is to make a test more readable by the addition of a well-known structure. If you only have two or three lines of code, odds are that the test is so small that it’s already readable as is.

4.2.3 Moderation of static analysis

While listing 4.1 is only a few lines of code, I still consider it too verbose. Particularly the act section could be more readable. There are two problems:

1. The call to ConfigureAwait adds what seems like redundant noise.

2. That’s quite a convoluted way to pass an empty string as an argument.

Let’s address each in turn.

If ConfigureAwait is redundant, then why is it there? It’s there because otherwise the code doesn’t compile. I’ve configured the test project according to the new-code-base checklist, which includes adding static code analysis and turning all warnings into errors.

One of these rules3 recommends calling ConfigureAwait on awaited tasks. The rule comes with documentation that explains the motivation. In short, by default a task resumes on the thread that originally created it. By calling ConfigureAwait(false) you indicate that the task can instead resume on any thread. This can avoid deadlocks and certain performance issues. The rule strongly suggests to call this method in code that implements a reusable library.

3 CA2007: Do not directly await a Task

A test library, however, isn’t a generally reusable library. The clients are known in advance: two or three standard test runners, including the built-in Visual Studio test runner, and the one used by your Continuous Integration server.

The documentation for the rule also contains a section on when it’s safe to deactivate the rule. A unit testing library fits the description, so you can turn it off to remove the noise from your tests.

Be aware that while it’s fine to turn off this particular rule for unit tests, it should remain in effect for production code. Listing 4.3 shows the Characterisation Test after clean-up.

Another issue with listing 4.1 is that the GetAsync method includes an overload that takes a string instead of a Uri object. The test would be more readable with "" instead of new Uri("", UriKind.Relative). Alas, another static code analysis rule4 discourages use of that overload.

4 CA2234: Pass System.Uri objects instead of strings

You should avoid ‘stringly typed’[3] code5. Instead of passing strings around, you should favour objects with good encapsulation. I salute that design principle, so I have no intention to deactivate the rule, like I did with the rule about ConfigureAwait.

5 Also known as Primitive Obsession[34].

I do believe, however, that we can make a principled exception from the rule. As you may have noticed, you have to populate a Uri object with a string. The advantage of a Uri object over a string is that, at the receiving side, you know that an encapsulated object carries stronger guarantees than a string does6. At the site where you create the object, there’s little difference. Therefore, I think it’s fair to suppress the warning since the code contains a string literal - not a variable.

6 Read more about guarantees and encapsulation in chapter 5.

[Fact]
[SuppressMessage(
    "Usage", "CA2234:Pass system uri objects instead of strings",
    Justification = "URL isn't passed as variable, but as literal.")]
public async Task HomeIsOk()
{
    using var factory = new WebApplicationFactory<Startup>();
    var client = factory.CreateClient();

    var response = await client.GetAsync("");

    Assert.True(
        response.IsSuccessStatusCode,
        $"Actual status code: {response.StatusCode}.");
}

Listing 4.3: Test with relaxed code analysis rules. (Restaurant/d8167c3/Restaurant.RestApi.Tests/HomeTests.cs)

Listing 4.3 shows the result of suppressing the ConfigureAwait rule for all tests, and the Uri rule for the specific test. Notice that the act section shrunk from three to one line of code. Most importantly, the code is easier to read. The code I removed was (in this context) noise. Now it’s gone.

You can see that I suppressed the Uri recommendation by using an attribute on the test method. Notice that I supplied a written Justification of my decision. As I argued in chapter 3, the code is the only artefact that really matters. Future readers may need to understand why the code is organised as it is7.

7 You can typically reconstruct what changed from your Git history. It’s much harder to reconstruct why things changed.

Documentation should prioritise explaining why a decision was made, rather than what was decided.

As useful as static code analysis is, false positives come with the territory. It’s okay to disable rules or suppress specific warnings, but don’t do this lightly. At least, document why you decide to do it, and if possible, get feedback on the decision.

4.3 Outside-in

Now we’re up to speed. There’s a system that responds to HTTP requests (although it doesn’t do much) and there’s an automated test. That’s our Walking Skeleton[36].

The system ought to do something useful. In this chapter, the goal is to implement a vertical slice through the system, from HTTP boundary to data store. Recall from subsection 2.2.2 that the system should be a simple online restaurant reservation system. I think a good candidate for a slice is the ability to receive a valid reservation and save it in a database. Figure 4.4 illustrates the plan.

Images

Figure 4.4: The plan is to create a vertical slice through the system that receives a valid reservation and saves it in a database.

The system should be an HTTP API that receives and replies with JSON documents. This is how the rest of the world interacts with the system. That’s the contract with external clients, so it’s important that once you’ve established it, you keep it.

How do you prevent regressions in the contract? One way is to write a set of automated tests against the HTTP boundary. If you write the tests before the implementation, then you have a driver for it.

Such a test can serve double duty as an automated acceptance test[49], so you might call the process acceptance-test-driven development. I prefer to call it outside-in test-driven development8, because while you begin at the boundary, you can (and should) work your way in. You’ll see an example of this soon.

8 I didn’t invent this term, but I don’t recall where I first heard it. The idea, however, I first encountered in Growing Object-Oriented Software, Guided by Tests[36].

4.3.1 Receive JSON

When you’re beginning a new code base, there’s so much that has to be done. It can be hard to move in small steps, but try, nonetheless. The smallest change I can think of in the restaurant reservation example is to verify that the response from the API is a JSON document.

We know that right now, it isn’t. At the moment, the web application just returns the hard-coded string Hello World! as a plain-text document.

In good test-driven style, you could write a new test that asserts that the response should be in JSON, but most of it would repeat the existing test shown in listing 4.3. Instead of duplicating the test code, you can elaborate on the existing test. Listing 4.4 shows the expanded test.

Three things have changed:

1. I changed the name of the test to be more specific.

2. The test now explicitly sets the request’s Accept header to application/json.

3. I added a second assertion.

By setting the Accept header, the client engages HTTP’s content negotiation[2] protocol. If the server can serve a JSON response, it ought to do so.

To verify that, I added a second assertion that examines the response’s Content-Type9.

9 You may have heard that a test should have only one assertion. You may also have heard that multiple assertions is called Assertion Roulette, and that it’s a code smell. Assertion Roulette is, indeed, a code smell, but multiple assertions per test isn’t necessarily an example of it. Assertion Roulette is either when you repeatedly interleave assert sections with additional arrange and act code, or when an assertion lacks an informative assertion message[66].

The test now fails at the second assertion. It expects the Content-Type header to be application/json, but it’s actually null. This is more like test-driven development: write a failing test, then make it pass.

When working with ASP.NET you’re expected to follow the Model View Controller[33] (MVC) pattern. Listing 4.5 shows the simplest Controller implementation I could pull off.

[Fact]
[SuppressMessage(
    "Usage", "CA2234:Pass system uri objects instead of strings",
    Justification = "URL isn't passed as variable, but as literal.")]
public async Task HomeReturnsJson()
{
    using var factory = new WebApplicationFactory<Startup>();
    var client = factory.CreateClient();

    using var request = new HttpRequestMessage(HttpMethod.Get, "");
    request.Headers.Accept.ParseAdd("application/json");
    var response = await client.SendAsync(request);

    Assert.True(
        response.IsSuccessStatusCode,
        $"Actual status code: {response.StatusCode}.");
    Assert.Equal(
        "application/json",
        response.Content.Headers.ContentType?.MediaType);
}

Listing 4.4: Test that asserts that the home resource returns JSON. (Restaurant/316beab/Restaurant.RestApi.Tests/HomeTests.cs)

[Route("")]
public class HomeController : ControllerBase
{
    public IActionResult Get()
    {
        return Ok(new { message = "Hello, World!"});
    }
}

Listing 4.5: First incarnation of HomeController. (Restaurant/316beab/Restaurant.RestApi/HomeController.cs)

This, in itself, however, isn’t enough. You also have to tell ASP.NET to use its MVC framework. You can do this in the Startup class, as shown in listing 4.6.

public sealed class Startup
{
    public static void ConfigureServices(IServiceCollection services)
    {
        services.AddControllers();
    }
    public static void Configure(
        IApplicationBuilder app,
        IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
            app.UseDeveloperExceptionPage();

        app.UseRouting();
        app.UseEndpoints(endpoints => { endpoints.MapControllers(); });
    }
}

Listing 4.6: Setting up ASP.NET for MVC. (Restaurant/316beab/Restaurant.RestApi/Startup.cs)

Compared to listing 2.5 this looks simpler. I consider that an improvement.

With these changes, the test in listing 4.4 passes. Commit the changes to Git, and consider pushing them through your deployment pipeline[49].

4.3.2 Post a reservation

Recall that the purpose of a vertical slice is to demonstrate that the system works. We’ve spent some time getting into position. That’s normal with a new code base, but now it’s ready.

When picking a feature for the first vertical slice, I look for a few things. You could call this a heuristic as well.

1. The feature should be simple to implement.

2. Prefer data input if possible.

When developing systems with persisted data, you quickly find that you need some data in the system in order to test other things. Starting with a feature that adds data to the system neatly addresses that concern.

In that light, it seems useful to enable the web application to receive and save a restaurant reservation. Using outside-in test-driven development, you could write a test like listing 4.7.

[Fact]
public async Task PostValidReservation()
{
    var response = await PostReservation(new {
        date = "2023-03-10 19:00",
        email = "[email protected]",
        name = "Katinka Ingabogovinanana",
        quantity = 2 });

    Assert.True(
        response.IsSuccessStatusCode,
        $"Actual status code: {response.StatusCode}.");
}

Listing 4.7: Testing that a valid reservation can be posted to the HTTP API. The PostReservation method is in listing 4.8. (Restaurant/90e4869/Restaurant.RestApi.Tests/ReservationsTests.cs)

When pursuing a vertical slice, aim for the happy path[66]. For now, ignore all the things that could go wrong10. The goal is to demonstrate that the system has a specific capability. In this example, the desired capability is to receive and save a reservation.

10 But if you think of any, write them down so you don’t forget about them[9].

Thus, listing 4.7 posts a valid reservation to the service. The reservation should include a valid date, email, name, and quantity. The test uses an anonymous type to emulate a JSON object. When serialised, the resulting JSON has the same structure, and the same field names.

High-level tests should go easy on the assertions. During development, many details will change. If you make the assertions too specific, you’ll have to correct them often. Better to keep a light touch. The test in listing 4.7 only verifies that the HTTP status code represents success, as discussed in section 4.2.1. As you add more test code, you’ll be describing the expected behaviour of the system in increasing detail. Do this iteratively.

You may have noticed that the test delegates all the action to a method called PostReservation. This is the Test Utility Method[66] shown in listing 4.8.

[SuppressMessage(
    "Usage",
    "CA2234:Pass system uri objects instead of strings",
    Justification = "URL isn't passed as variable, but as literal.")]
private async Task<HttpResponseMessage> PostReservation(
    object reservation)
{
    using var factory = new WebApplicationFactory<Startup>();
    var client = factory.CreateClient();

    string json = JsonSerializer.Serialize(reservation);
    using var content = new StringContent(json);
    content.Headers.ContentType.MediaType = "application/json";
    return await client.PostAsync("reservations", content);
}

Listing 4.8: PostReservation helper method. This method is defined in the test code base. (Restaurant/90e4869/Restaurant.RestApi.Tests/ReservationsTests.cs)

Much of the code is similar to listing 4.4. I could have written it in the test itself. Why didn’t I? There’s a couple of reasons, but this is where software engineering is more art than science.

One reason is that I think it makes the test itself more readable. Only the essentials are visible. You post some values to the service, the response indicates success. This is a great example of an abstraction, according to Robert C. Martin:

“Abstraction is the elimination of the irrelevant and the amplification of the essential”[60]

Another reason I wanted to define a helper method is that I’d like to reserve the right to change how this is done. Notice that the last line of code calls PostAsync with the hard-coded relative path "reservations". This implies that the reservations resource exists at a URL like https://api.example.com/reservations This might be the case, but you may not want this to be part of your contract.

You can write an HTTP API with published URL templates, but it wouldn’t be REST because it’s hard to change the API without breaking the contract[2]. APIs that expect clients to use documented URL templates use HTTP verbs, but not hypermedia controls11.

11 The Richardson Maturity Model for REST distinguishes between three levels: 1. Resources. 2. HTTP verbs. 3. Hypermedia controls[113].

It’s too big a detour to insist on hypermedia controls (i.e. links) right now, so in order to reserve the right to change things later, you can abstract the service interaction in a SUT12 Encapsulation Method[66].

12 SUT: System Under Test.

The only other remark I’ll make about listing 4.8 is that I chose to suppress the code analysis rule that suggests Uri objects, for the same reason as explained in section 4.2.3.

When you run the test it fails as expected. The Assertion Message[66] is Actual status code: NotFound. This means that the /reservations resource doesn’t exist on the server. Hardly surprising, since we’ve yet to implement it.

That’s straightforward to do, as listing 4.9 shows. It’s the minimal implementation that passes all existing tests.

    [Route("[controller]")]
    public class ReservationsController
    {
#pragma warning disable CA1822 // Mark members as static
        public void Post() { }
#pragma warning restore CA1822 // Mark members as static
    }

Listing 4.9: Minimal ReservationsController. (Restaurant/90e4869/Restaurant.RestApi/ReservationsController.cs)

The first detail that you see is the ugly #pragma instructions. As their comments suggest, they suppress a static code analysis rule that insists on making the Post method static. You can’t do that, though: If you make the method static then the test fails. The ASP.NET MVC framework matches HTTP requests with controller methods by convention, and methods must be instance methods (i.e. not static).

There’s multiple ways to suppress warnings from the .NET analysers and I deliberately chose the ghastliest alternative. I did that instead of leaving a //TODO comment. I hope those #pragma instructions have the same effect.

The Post method is currently a no-op, but it obviously shouldn’t stay like that. You have to temporarily suppress the warning, though, because otherwise the code doesn’t compile. Treating warnings as errors isn’t a free ride, but I find the slowdown worthwhile. Remember: the goal isn’t to write as many lines of code as fast as you can. The goal is sustainable software.

The goal it not to write code fast. The goal is sustainable software.

All tests now pass. Commit the changes in Git, and consider pushing them through your deployment pipeline[49].

4.3.3 Unit test

As listing 4.9 shows, the web service doesn’t handle the posted reservation. You can use another test to drive the behaviour closer to the goal, like the test in listing 4.10.

[Fact]
public async Task PostValidReservationWhenDatabaseIsEmpty()
{
    var db = new FakeDatabase();
    var sut = new ReservationsController(db);

    var dto = new ReservationDto
    {
        At = "2023-11-24 19:00",
        Email = "[email protected]",
        Name = "Julia Domna",
        Quantity = 5
    };
    await sut.Post(dto);

    var expected = new Reservation(
        new DateTime(2023, 11, 24, 19, 0, 0),
        dto.Email,
        dto.Name,
        dto.Quantity);
    Assert.Contains(expected, db);
}

Listing 4.10: Unit test of posting a valid reservation. (Restaurant/bc1079a/Restaurant.RestApi.Tests/ReservationsTests.cs)

Unlike the previous tests you’ve seen, this isn’t a test against the system’s HTTP API. It’s a unit test13. This illustrates the key idea behind outside-in test-driven development. While you start at the boundary of the system, you should work your way in.

13 The term unit test is ill-defined. There’s little consensus about its definition. I lean towards defining it as a an automated test that tests a unit in isolation of its dependencies. Note that this definition is still vague, since it doesn’t define unit. I normally think of a unit as a small piece of behaviour, but exactly how small is, again, ill-defined.

“But the boundary of the system is where the system interacts with the outside world,” you object, “Shouldn’t we be testing its behaviour?

That sounds appropriate, but is, unfortunately, impractical. Trying to cover all behaviour and edge cases via boundary tests leads to a combinatorial explosion. You’d have to write tens of thousands of tests to do this[84]. Going from testing the outside to testing units in isolation addresses that problem.

While the unit test in listing 4.10 looks simple on the surface, much is going on behind the scenes. It’s another example of an abstraction: amplify the essentials and eliminate the irrelevant. Clearly, no code is irrelevant. The point is that in order to understand the overall purpose of the test, you don’t (yet) need to understand all the details of ReservationDto, Reservation, or FakeDatabase.

The test is structured according to the Arrange Act Assert[9] heuristic[91]. A blank line separates each phase. The arrange phase creates a FakeDatabase as well as the System Under Test (SUT)[66].

The act phase creates a Data Transfer Object (DTO)[33] and passes it to the Post method. You could also have created the dto as part of the arrange phase. I think you can argue for both alternatives, so I tend to go with what balances best, as I described in section 4.2.2. In this case there’s two statements in each phase. I think that this 2-2-2 structure balances better than the 3-1-2 shape that would result if you move initialisation of the dto to the arrange phase.

Finally, the assert phase verifies that the database contains the expected reservation.

This describes the overall flow of the test, as well as the reason that it’s structured the way it is. Hopefully, the abstractions introduced here enabled you to follow along even though you have yet to see the new classes. Before consulting listing 4.11, imagine what ReservationDto looks like.

4.3.4 DTO and Domain Model

When you looked, were you surprised? It’s a completely normal C# DTO. Its only responsibility is to mirror the structure of the incoming JSON document and capture its constituent values.

public class ReservationDto
{
    public string? At { get; set; }
    public string? Email { get; set; }
    public string? Name { get; set; }
    public int Quantity { get; set; }
}

Listing 4.11: Reservation DTO. This is part of the production code. (Restaurant/bc1079a/Restaurant.RestApi/ReservationDto.cs)

How do you think Reservation looks? Why does the code even contain two classes with similar names? The reason is that while they both represent a reservation, they play different roles.

The role of a DTO is to capture incoming data in a data structure, or to help transform a data structure to output. You should use it for nothing else, as it offers no encapsulation. Martin Fowler puts it this way:

“A Data Transfer Object is one of those objects our mothers told us never to write.”[33]

The purpose of the Reservation class, on the other hand, is to encapsulate the business rules that apply to a reservation. It’s part of the code’s Domain Model[33][26]. Listing 4.12 shows the initial version of it. While it looks more complex14 than listing 4.11, it actually isn’t. It’s made from exactly the same number of constituent parts.

14 Keep in mind that I use the word complex to mean assembled from parts[45]. It’s not a synonym for complicated.

“But there’s so much more code there! Didn’t you cheat? Where are the tests that drove you to this implementation?” you ask.

I wrote no tests of the Reservation class (apart from listing 4.10). I never claimed that I’d stick strictly to test-driven development.

Earlier in this chapter I discussed how I don’t trust myself to write correct code. Again, recall the bat-and-ball problem if you need a reminder of how easily fooled the brain is. I do, however, trust a tool to write code for me. While I’m not a big fan of auto-generated code, Visual Studio wrote most of listing 4.12 for me.

I wrote the four read-only properties and then used Visual Studio’s generate constructor tool to add the constructor, and the generate Equals and GetHashCode tool for the rest. I trust that Microsoft tests the features they include in their products.

public sealed class Reservation
{
    public Reservation(
        DateTime at,
        string email,
        string name,
        int quantity)
    {
        At = at;
        Email = email;
        Name = name;
        Quantity = quantity;
    }

    public DateTime At { get; }
    public string Email { get; }
    public string Name { get; }
    public int Quantity { get; }

    public override bool Equals(object? obj)
    {
        return obj is Reservation reservation &&
               At == reservation.At &&
               Email == reservation.Email &&
               Name == reservation.Name &&
               Quantity == reservation.Quantity;
    }

    public override int GetHashCode()
    {
        return HashCode.Combine(At, Email, Name, Quantity);
    }
}

Listing 4.12: Reservation class. This is part of the Domain Model. (Restaurant/bc1079a/Restaurant.RestApi/Reservation.cs)

How does Reservation better encapsulate the business rules about reservations? For now, it barely does. The major difference is that, as opposed to the DTO, the domain object requires all four constituent values to be present15. In addition, the Date is declared as a DateTime, which guarantees that the value is a proper date, and not just any arbitrary string. If you aren’t yet convinced, section 5.3 and subsection 7.2.5 returns to the Reservation class to make it more compelling.

15 Recall that the nullable reference types feature is enabled. The absence of question marks in the property declarations indicate that none of them may be null. Contrast with listing 4.11, which has question marks on all properties, indicating that all may be null.

Why does Reservation look like a Value Object16? Because this offers a number of advantages. You should prefer Value Objects for your Domain Model[26]. It also makes testing easier[103].

16 A Value Object[33] is an immutable object that composes other values and make them look like a single, albeit complex, value. The archetypical example would be a Money class consisting of a currency and an amount[33].

Consider the assertion in listing 4.10. It looks for expected in db. How did expected get into db? It didn’t, but an object that looks just like it did. Assertions use objects’ own definitions of equality to compare expected and actual values, and Reservation overrides Equals. You can only safely implement such structural equality when the class is immutable. Otherwise, you might compare two mutable objects and think they’re the same, only to later see them diverge.

Structural equality makes elegant assertions possible[103]. In the test, just create an object that represents the expected outcome, and compare it to the actual result.

4.3.5 Fake Object

The final new class implied by listing 4.10 is FakeDatabase, shown in listing 4.13. As its name implies, this is a Fake Object[66], a kind of Test Double[66]17. It pretends that it’s a database.

17 You may know Test Doubles as mocks and stubs. Like the word unit test, there’s no consensus about what these words actually mean. I try to avoid them for that reason. For what it’s worth, the excellent book xUnit Test Patterns[66] offers clear definitions of those terms, but alas, no-one uses them.

It’s just an ordinary in-memory collection that implements an interface called IReservationsRepository. Since it derives from Collection<Reservation> it comes with various collection methods, including Add. That’s also the reason that it works with Assert.Contains in listing 4.10.

[SuppressMessage(
    "Naming",
    "CA1710:Identifiers should have correct suffix",
    Justification = "The role of the class is a Test Double.")]
public class FakeDatabase :
    Collection<Reservation>, IReservationsRepository
{
    public Task Create(Reservation reservation)
    {
        Add(reservation);
        return Task.CompletedTask;
    }
}

Listing 4.13: Fake database. This is part of the test code. (Restaurant/bc1079a/Restaurant.RestApi.Tests/FakeDatabase.cs)

A Fake Object[66] is a test-specific object that nonetheless has proper behaviour. When you use it as a stand-in for a real database, you can think of it as a kind of in-memory database. It works well with state-based testing[99]. That’s the kind of test shown in listing 4.10. In the assert phase, you verify that the actual state fits with the expected state. That particular test considers the state of db.

4.3.6 Repository Interface

The FakeDatabase class implements the IReservationsRepository interface shown in listing 4.14. This early in the lifetime of the code base the interface only defines a single method.

public interface IReservationsRepository
{
    Task Create(Reservation reservation);
}

Listing 4.14: Repository interface. This is part of the Domain Model. (Restaurant/bc1079a/Restaurant.RestApi/IReservationsRepository.cs)

For now, I chose to name the interface after the Repository pattern[33], although it only has a passing similarity to the original pattern description. I did that because most people are familiar with the name and understand that it models data access in some way. I may decide to rename it later.

4.3.7 Create in Repository

As you can see by the distance between this page and listing 4.10, that single test sparked the creation of several new types. This is normal early in the life of a code base. There’s almost no existing code, so even a simple test is likely to set off a small avalanche of new code.

Based on the test, you also have to modify ReservationsController’s constructor and Post method to support the interaction driven by the test. The constructor must take an IReservationsRepository parameter, and the Post method a ReservationDto parameter. Once you’ve made these changes, the test finally compiles so that you can run it.

When you execute it, it fails, as it’s supposed to do.

To make it pass, you must add a Reservation object to the repository in the Post method. Listing 4.15 shows how.

The ReservationsController uses Constructor Injection[25] to receive the injected repository and save it as a read-only property for later use. This means that in any properly initialised instance of the class, the Post method can use it. Here, it calls Create with a hard-coded Reservation. While this is obviously wrong, it passes the test. It’s the simplest18 thing that could possibly work[22].

18 You might argue that it’d be just as simple to copy the values from dto. It’s true that this would have the same cyclomatic complexity and the same number of lines of code, but in the spirit of the The Transformation Priority Premise[64] (TPP), I consider a constant to be simpler than a variable. See subsection 5.1.1 for more details on the TPP.

If you’re wondering what drove the Guard Clause[7] against null into existence, that was prompted by a static code analysis rule. Again, keep in mind that you can use more than one driver at the same time: test-driven development and analysers or linters. There’s lots of tooling that can drive creation of code. In fact, I used Visual Studio’s add null check tool to add the guard.

The code in listing 4.15 passes the test in listing 4.10, but now another test fails!

4.3.8 Configure dependencies

While the new test succeeds, the boundary test in listing 4.7 now fails because ReservationsController no longer has a parameterless constructor. The ASP.NET framework needs help creating instances of the class, particularly because no classes in the production code implement the required IReservationsRepository interface.

[ApiController, Route("[controller]")]
public class ReservationsController
{
    public ReservationsController(IReservationsRepository repository)
    {
        Repository = repository;
    }

    public IReservationsRepository Repository { get; }

    public async Task Post(ReservationDto dto)
    {
        if (dto is null)
            throw new ArgumentNullException(nameof(dto));

        await Repository
            .Create(
                new Reservation(
                    new DateTime(2023, 11, 24, 19, 0, 0),
                    "[email protected]",
                    "Julia Domna",
                    5))
                .ConfigureAwait(false);
    }
}

Listing 4.15: Saving a reservation in the injected repository. (Restaurant/bc1079a/Restaurant.RestApi/ReservationsController.cs)

The simplest way to make all tests pass is to add a Null Object[117] implementation of the interface. Listing 4.16 shows a temporary class nested within the Startup class. It’s an implementation of IReservationsRepository that doesn’t do anything.

private class NullRepository : IReservationsRepository
{
    public Task Create(Reservation reservation)
    {
        return Task.CompletedTask;
    }
}

Listing 4.16: Null Object implementation. This is a temporary, nested private class. (Restaurant/bc1079a/Restaurant.RestApi/Startup.cs)

If you register it with ASP.NET’s built-in Dependency Injection Container[25] it’ll solve the problem. Listing 4.17 shows how to do that. Since NullRepository is stateless, you can register a single object with the Singleton lifetime[25], which means that the same object will be shared between all threads during the process lifetime of the web service.

public static void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();

    services.AddSingleton<IReservationsRepository>(
        new NullRepository());
}

Listing 4.17: Register NullRepository with ASP.NET’s built-in DI Container. (Restaurant/bc1079a/Restaurant.RestApi/Startup.cs)

All tests now pass. Commit the changes in Git, and consider pushing them through your deployment pipeline.

4.4 Complete the slice

Pursuing the vertical slice, figure 4.5 implies that something is missing. You need a proper implementation of IReservationsRepository to save the reservation to persistent storage. Once you have that, you’ve completed the slice.

Images

Figure 4.5: Progress so far. Compare to the plan shown in figure 4.4.

“Wait a minute,” you say, “it doesn’t work at all! It’d just save a hard-coded reservation! And what about input validation, logging, or security?”

We’ll get to all of this in time. Right now, I’ll be satisfied if a stimulus can produce a persistent state change, even if that’s a hard-coded reservation. It’ll still demonstrate that an external event (an HTTP POST) can modify the state of the application.

4.4.1 Schema

How should we save the reservation? In a relational database? A graph database[88]? A document database?

If you were to follow the spirit of Growing Object-Oriented Software, Guided by Tests[36] (GOOS) you should pick the technology that best supports test-driven development. Preferably something that you can host within your automated tests. That suggests a document database.

Despite of this, I’ll pick a relational database - specifically, SQL Server. I do this for educational reasons. First, GOOS[36] is already an excellent resource if you want to learn principled outside-in test-driven development. Second, in reality relational databases are ubiquitous. Having a relational database is often non-negotiable. Your organisation may have a support agreement with a particular vendor. Your operations team may prefer a specific system because they know how to maintain it, and do backups. Your colleagues may be most comfortable with a certain database.

Despite the NoSQL movement, relational databases remain an unavoidable part of enterprise software development. I hope that this book is more useful because I include a relational database as part of the example. I’ll use SQL Server because it’s an idiomatic part of the standard Microsoft stack, but the techniques you’d have to apply wouldn’t change much if you chose another database.

Listing 4.18 shows the initial schema for the Reservations table.

CREATE TABLE [dbo].[Reservations] (
    [Id]         INT                NOT NULL IDENTITY,
    [At]         DATETIME2          NOT NULL,
    [Name]       NVARCHAR (50)      NOT NULL,
    [Email]      NVARCHAR (50)      NOT NULL,
    [Quantity]   INT                NOT NULL
    PRIMARY KEY CLUSTERED([Id] ASC)
)

Listing 4.18: Database schema for the Reservations table. (Restaurant/c82d82c/Restaurant.RestApi/RestaurantDbSchema.sql)

I prefer defining a database schema in SQL, since that’s the native language of the database. If you instead prefer to use an object-relational mapper or a domain-specific language then that’s fine too. The important part is that you commit the database schema to the same Git repository that holds all the other source code.

Commit database schema to the Git repository.

4.4.2 SQL Repository

Now that you know what the database schema looks like, you can implement the IReservationsRepository interface against the database. Listing 4.19 shows my implementation. As you can tell, I’m not a fan of object-relational mappers (ORMs).

You may argue that using the fundamental ADO.NET API is verbose compared to, say, Entity Framework, but keep in mind that you shouldn’t be optimising for writing speed. When optimising for readability, you can still argue that using an object-relational mapper would be more readable. I think that there’s a degree of subjective judgment involved.

public class SqlReservationsRepository : IReservationsRepository
{
    public SqlReservationsRepository(string connectionString)
    {
        ConnectionString = connectionString;
    }

    public string ConnectionString { get; }

    public async Task Create(Reservation reservation)
    {
        if (reservation is null)
            throw new ArgumentNullException(nameof(reservation));

        using var conn = new SqlConnection(ConnectionString);
        using var cmd = new SqlCommand(createReservationSql, conn);
        cmd.Parameters.Add(new SqlParameter("@At", reservation.At));
        cmd.Parameters.Add(new SqlParameter("@Name", reservation.Name));
        cmd.Parameters.Add(new SqlParameter("@Email", reservation.Email));
        cmd.Parameters.Add(
            new SqlParameter("@Quantity", reservation.Quantity));

        await conn.OpenAsync().ConfigureAwait(false);
        await cmd.ExecuteNonQueryAsync().ConfigureAwait(false);
    }

    private const string createReservationSql = @"
        INSERT INTO
            [dbo].[Reservations] ([At], [Name], [Email], [Quantity])
        VALUES (@At, @Name, @Email, @Quantity)";
}

Listing 4.19: SQL Server implementation of the Repository interface. (Restaurant/c82d82c/Restaurant.RestApi/SqlReservationsRepository.cs)

If you want to use an object-relational mapper instead, then do so. That’s not the important point. The important point is that you keep your Domain Model[33] unpolluted by implementation details19.

19 This is the Dependency Inversion Principle applied. Abstractions should not depend upon details. Details should depend upon abstractions[60]. The abstraction in this context is the Domain Model, i.e. Reservation.

What I like about the implementation in listing 4.19 is that it has simple invariants. It’s a stateless, thread-safe object. You can create a single instance of it and reuse it during the lifetime of your application.

“But Mark,” you protest, “now you’re cheating again! You didn’t test-drive that class.”

True, I didn’t do that because I consider SqlReservationsRepository a Humble Object[66]. This is an implementation that’s hard to unit test because it depends on a subsystem that you can’t easily automate. Instead, you drain the object of branching logic and other kinds of behaviour that tends to cause defects.

The only branching in SqlReservationsRepository is the null guard that was driven by static code analysis and created by Visual Studio.

All that said, in section 12.2 you’ll see how to add automated tests that involve the database.

4.4.3 Configuration with database

Now that you have a proper implementation of IReservationsRepository you have to tell ASP.NET about it. Listing 4.20 shows the changes you need to make to the Startup class.

You call AddSingleton with the new SqlReservationsRepository class instead of the NullRepository class from listing 4.16. That class you can now delete.

You can’t create a SqlReservationsRepository instance unless you supply a connection string, so you must get that from the ASP.NET’s configuration. When you add a constructor to Startup, like shown in listing 4.20, the framework automatically supplies an instance of IConfiguration.

You’ll have to configure the application with a proper connection string. Among the many options available, you can use a configuration file. Listing 4.21 shows what I commit to Git at this point. While it’s helpful to your colleagues to commit the structure of required configuration, don’t include actual connection strings. They’re going to vary according to environments and may contain secrets that shouldn’t be in your version control system.

If you put a real connection string in the configuration file, the application ought to work.

public IConfiguration Configuration { get; }

public Startup(IConfiguration configuration)
{
    Configuration = configuration;
}

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();

    var connStr = Configuration.GetConnectionString("Restaurant");
    services.AddSingleton<IReservationsRepository>(
        new SqlReservationsRepository(connStr));
}

Listing 4.20: The parts of the Startup file that configure the application to run against SQL Server. (Restaurant/c82d82c/Restaurant.RestApi/Startup.cs)

{
  "ConnectionStrings": {
    "Restaurant": ""
  }
}

Listing 4.21: Structure of connection string configuration. This is what you should commit to Git. Be sure to avoid committing secrets. (Restaurant/c82d82c/Restaurant.RestApi/appsettings.json)

4.4.4 Perform a Smoke Test

How do you know that the software works? After all, we didn’t add an automated systems test.

While you should favour automated tests, you shouldn’t forget manual testing. Once in a while, turn the system on and see if it catches fire. This is called a Smoke Test.

If you put a proper connection string in the configuration file and start the system on your development machine, you can try to POST a reservation to it. There’s a wide selection of tools available to interact with an HTTP API. .NET developers tend to prefer GUI-based tools like Postman or Fiddler, but do yourself a favour and learn to use something that’s easier to automate. I often use cURL. Here’s an example (broken into multiple lines to fit the page):

$ curl -v http://localhost:53568/reservations
  -H "Content-Type: application/json"
  -d "{ "date": "2022-10-21 19:00",
        "email": "[email protected]",
        "name": "Cara van Palace",
        "quantity": 3 }"

This posts a JSON reservation to the appropriate URL. If you look in the database you configured the application to use, you should now see a row with a reservation... for Julia Domna!.

Recall that the system still saves a hard-coded reservation, but at least you now know that if you supply a stimulus, something happens.

4.4.5 Boundary test with Fake database

The only remaining problem is that the boundary test in listing 4.7 now fails. The Startup class configures the SqlReservationsRepository service with a connection string, but there’s no connection string in the test context. There’s also no database.

It’s possible to automate setting up and tearing down a database for automated test purposes, but it’s cumbersome and slows down the tests. Maybe later20, but not now.

20 In section 12.2, in fact.

Instead, you can run the boundary test against the FakeDatabase shown in listing 4.13. In order to do that, you must change how the test’s WebApplicationFactory behaves. Listing 4.22 shows how to override its ConfigureWebHost method.

public class RestaurantApiFactory : WebApplicationFactory<Startup>
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        if (builder is null)
            throw new ArgumentNullException(nameof(builder));

        builder.ConfigureServices(services =>
        {
            services.RemoveAll<IReservationsRepository>();
            services.AddSingleton<IReservationsRepository>(
                new FakeDatabase());
        });
    }
}

Listing 4.22: How to replace a real dependency with a Fake for testing purposes. (Restaurant/c82d82c/Restaurant.RestApi.Tests/RestaurantApiFactory.cs)

The code in the ConfigureServices block runs after the Startup class’ ConfigureServices method has executed. It finds all the services that implement the IReservationsRepository interface (there’s only one) and removes them. It then adds a FakeDatabase instance as a replacement.

You have to use the new RestaurantApiFactory class in your unit test, but that’s just a change to a single line in the PostReservation helper method. Compare listing 4.23 with listing 4.8.

Once more, all tests pass. Commit the changes in Git, and push them through your deployment pipeline. Once the changes are in production, perform another manual Smoke Test against the production system.

4.5 Conclusion

A thin vertical slice is an effective way to demonstrate that the software may actually work. Combined with Continuous Delivery[49] you’re able to quickly put working software in production.

You may think that the first vertical slice is so ‘thin’ that it’s pointless. The example in this chapter showed how to save a reservation in a database, but the values being saved aren’t the values supplied to the system. How does that add any value?

[SuppressMessage(
    "Usage",
    "CA2234:Pass system uri objects instead of strings",
    Justification = "URL isn't passed as variable, but as literal.")]
private async Task<HttpResponseMessage> PostReservation(
    object reservation)
{
    using var factory = new RestaurantApiFactory();           
    var client = factory.CreateClient();

    string json = JsonSerializer.Serialize(reservation);
    using var content = new StringContent(json);
    content.Headers.ContentType.MediaType = "application/json";
    return await client.PostAsync("reservations", content);
}

Listing 4.23: Test helper method with updated web application factory. Only the highlighted line that initialises the factory has changed compared to listing 4.8. (Restaurant/c82d82c/Restaurant.RestApi.Tests/ReservationsTests.cs)

Granted, it hardly does, but it establishes a running system, as well as a deployment pipeline[49]. Now you can improve on it. Small improvements, continuously delivered, inches closer towards a useful system. Other stakeholders are better equipped to evaluate when the system becomes useful. Your task is to enable them to perform that evaluation. Deploy as often as you can, and let other stakeholders tell you when you’re done.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.92.84.253