Test-driving application logic
Johnny: What’s next on our TODO list?
Benjamin: We have…
- a single reminder to change the name of the factory that creates
ReservationInProgressinstances. - Also, we have two places where a
NotImplementedExceptionsuggests there’s something left to implement.- The first one is the mentioned factory.
- The second one is the
NewReservationCommandclass that we discovered when writing a Statement for the factory.
Johnny: Let’s do the NewReservationCommand. I suspect the complexity we put inside the controller will pay back here and we will be able to test-drive the command logic on our own terms.
Benjamin: Can’t wait to see that. Here’s the current code of the command:
1 public class NewReservationCommand : ReservationCommand
2 {
3 public void Execute()
4 {
5 throw new NotImplementedException();
6 }
7 }
Johnny: So we have the class and method signature. It seems we can use the usual strategy of writing new Statement.
Benjamin: You mean “start by invoking a method if you have one”? I thought of the same. Let me try it.
1 public class NewReservationCommandSpecification
2 {
3 [Fact] public void
4 ShouldXXXXXXXXXXX() //TODO change the name
5 {
6 //GIVEN
7 var command = new NewReservationCommand();
8
9 //WHEN
10 command.Execute();
11
12 //THEN
13 Assert.True(false); //TODO unfinished
14 }
15 }
This is what I could write almost brain-dead. I just took the parts we have and put them in the Statement.
The assertion is still missing - the one we have is merely a placeholder. This is the right time to think what should happen when we execute the command.
Johnny: To come up with that expected behavior, we need to look back to the input data for the whole use case. We passed it to the factory and forgot about it. So just as a reminder - here’s how it looks like:
1 public class ReservationRequestDto
2 {
3 public readonly string TrainId;
4 public readonly uint SeatCount;
5
6 public ReservationRequestDto(string trainId, uint seatCount)
7 {
8 TrainId = trainId;
9 SeatCount = seatCount;
10 }
11 }
The first part is train id – it says on which train we should reserve the seats. So we need to somehow pick a train from the fleet for reservation. Then, on that train, we need to reserve as many seats as the customer requests. The requested seat count is the second part of the user request.
Benjamin: Aren’t we going to update the data in some kind of persistent storage? I doubt that the railways company would want the reservation to disappear on application restart.
Johnny: Yes, we need to act as if there was some kind of persistence.
Given all of above, I can see two new roles in our scenario:
- A fleet - from which we pick the train and where we save our changes
- A train - which is going to handle the reservation logic.
Both of these roles need to be modeled as mocks, because I expect them to play active roles in this scenario.
Let’s expand our Statement with our discoveries.
1 [Fact] public void
2 ShouldReserveSeatsInSpecifiedTrainWhenExecuted()
3 {
4 //GIVEN
5 var command = new NewReservationCommand(fleet);
6
7 fleet.Pick(trainId).Returns(train);
8
9 //WHEN
10 command.Execute();
11
12 //THEN
13 Received.InOrder(() =>
14 {
15 train.ReserveSeats(seatCount);
16 fleet.UpdateInformationAbout(train);
17 };
18 }
Benjamin: I can see there are many things missing here. For instance, we don’t have the train and fleet variables.
Johnny: We created a need for them. I think we can safely introduce them into the Statement now.
1 [Fact] public void
2 ShouldReserveSeatsInSpecifiedTrainWhenExecuted()
3 {
4 //GIVEN
5 var fleet = Substitute.For<TrainFleet>();
6 var train = Substitute.For<ReservableTrain>();
7 var command = new NewReservationCommand(fleet);
8
9 fleet.Pick(trainId).Returns(train);
10
11 //WHEN
12 command.Execute();
13
14 //THEN
15 Received.InOrder(() =>
16 {
17 train.ReserveSeats(seatCount);
18 fleet.UpdateInformationAbout(train);
19 };
20 }
Benjamin: I see two new types: TrainFleet and ReservableTrain.
Johnny: They symbolize the roles we just discovered.
Benjamin: Also, I can see that you used Received.InOrder() from NSubstitute to specify the expected call order.
Johnny: That’s because we need to reserve the seats before we update the information in some kind of storage. If we got the order wrong, the change could be lost.
Benjamin: But something is missing in this Statement. I just looked at the outputs our users expect:
1 public class ReservationDto
2 {
3 public readonly string TrainId;
4 public readonly string ReservationId;
5 public readonly List<TicketDto> PerSeatTickets;
6
7 public ReservationDto(
8 string trainId,
9 List<TicketDto> perSeatTickets,
10 string reservationId)
11 {
12 TrainId = trainId;
13 PerSeatTickets = perSeatTickets;
14 ReservationId = reservationId;
15 }
16 }
That’s a lot of info we need to pass back to the user. How exactly are we going to do that when the train.ReserveSeats(seatCount) call you invented is not expected to return anything?
Johnny: Ah, yes, I almost forgot - we’ve got the ReservationInProgress instance that we passed to the factory, but not yet to the command, right? The ReservationInProgress was invented exactly for this purpose - to gather the information necessary to produce a result of the whole operation. Let me just quickly update the Statement:
1 [Fact] public void
2 ShouldReserveSeatsInSpecifiedTrainWhenExecuted()
3 {
4 //GIVEN
5 var fleet = Substitute.For<TrainFleet>();
6 var train = Substitute.For<ReservableTrain>();
7 var reservationInProgress = Any.Instance<ReservationInProgress>();
8 var command = new NewReservationCommand(fleet, reservationInProgress);
9
10 fleet.Pick(trainId).Returns(train);
11
12 //WHEN
13 command.Execute();
14
15 //THEN
16 Received.InOrder(() =>
17 {
18 train.ReserveSeats(seatCount, reservationInProgress);
19 fleet.UpdateInformationAbout(train);
20 };
21 }
Now the ReserveSeats method accepts reservationInProgress.
Benjamin: Why are you passing the reservationInProgress further to the ReserveSeats method?
Johnny: The command does not have the necessary information to fill the reservationInProgress once the reservation is successful. We need to defer it to the ReservableTrain implementations to further decide the best place to do that.
Benjamin: I see. Looking at the Statement again - we’re missing two more variables – trainId and seatCount – and not only their definitions, but also we don’t pass them to the command at all. They are only present in our assumptions and expectations.
Johnny: Right, let me correct that.
1 [Fact] public void
2 ShouldReserveSeatsInSpecifiedTrainWhenExecuted()
3 {
4 //GIVEN
5 var fleet = Substitute.For<TrainFleet>();
6 var train = Substitute.For<ReservableTrain>();
7 var trainId = Any.String();
8 var seatCount = Any.UnsignedInt();
9 var reservationInProgress = Any.Instance<ReservationInProgress>();
10 var command = new NewReservationCommand(
11 trainId,
12 seatCount,
13 fleet,
14 reservationInProgress);
15
16 fleet.Pick(trainId).Returns(train);
17
18 //WHEN
19 command.Execute();
20
21 //THEN
22 Received.InOrder(() =>
23 {
24 train.ReserveSeats(seatCount, reservationInProgress);
25 fleet.UpdateInformationAbout(train);
26 };
27 }
Benjamin: Why is seatCount a uint?
Johnny: Look it up in the DTO - it’s a uint there. I don’t see the need to redefine that here.
Benjamin: Fine, but what about the trainId - it’s a string. Didn’t you tell me we need to use domain-related value objects for concepts like this?
Johnny: Yes, and we will refactor this string into a value object, especially that we have a requirement that train id comparisons should be case-insensitive. But first, I want to finish this Statement before I go into defining and specifying a new type. Still, we’d best leave a TODO note to get back to it later:
1 var trainId = Any.String(); //TODO extract value object
So far so good, I think we have a complete Statement. Want to take the keyboard?
Benjamin: Thanks. Let’s start implementing it then. First, I will start with these two interfaces:
1 var fleet = Substitute.For<TrainFleet>();
2 var train = Substitute.For<ReservableTrain>();
They don’t exist, so this code doesn’t compile. I can easily fix this by creating the interfaces in the production code:
1 public interface TrainFleet
2 {
3 }
4
5 public interface ReservableTrain
6 {
7 }
Now for this part:
1 var command = new NewReservationCommand(
2 trainId,
3 seatCount,
4 fleet,
5 reservationInProgress);
It doesn’t compile because the command does not accept any constructor parameters yet. Let’s create a fitting constructor, then:
1 public class NewReservationCommand : ReservationCommand
2 {
3 public NewReservationCommand(
4 string trainId,
5 uint seatCount,
6 TrainFleet fleet,
7 ReservationInProgress reservationInProgress)
8 {
9
10 }
11
12 public void Execute()
13 {
14 throw new NotImplementedException();
15 }
16 }
Our Statement can now invoke this constructor, but we broke the TicketOfficeCommandFactory which also creates a NewReservationCommand instance. Look:
1 public ReservationCommand CreateNewReservationCommand(
2 ReservationRequestDto requestDto,
3 ReservationInProgress reservationInProgress)
4 {
5 //Stopped compiling:
6 return new NewReservationCommand();
7 }
Johnny: We need to fix the factory the same way we needed to fix the composition root when test-driving the controller. Let’s see… Here:
1 public ReservationCommand CreateNewReservationCommand(
2 ReservationRequestDto requestDto,
3 ReservationInProgress reservationInProgress)
4 {
5 return new NewReservationCommand(
6 requestDto.TrainId,
7 requestDto.SeatCount,
8 new TodoTrainFleet(), // TODO fix name and scope
9 reservatonInProgress
10 );
11 }
Benjamin: The parameter passing looks straightforward to me except the TodoTrainFleet() – I already know that the name is a placeholder - you already did something like that earlier. But what about the lifetime scope?
Johnny: It’s also a placeholder. For now, I want to make the compiler happy, at the same time keeping existing Statements true and introducing a new class – TodoTrainFleet – that will bring new items to our TODO list.
Benjamin: New TODO items?
Johnny: Yes. Look – the type TodoTrainFleet does not exist yet. I’ll create it now:
1 public class TodoTrainFleet
2 {
3
4 }
This doesn’t match the signature of the command constructor, which expects a TrainFleet, so I need to make TodoTrainFleet implement this interface:
1 public class TodoTrainFleet : TrainFleet
2 {
3
4 }
Now I am forced to implement the methods from the TrainFleet interface. Although this interface doesn’t define any methods yet, we already discovered two in our Statement, so it will shortly need to get them to make the compiler happy. They will both contain code throwing NotImplementedException, which will land on the TODO list.
Benjamin: I see. Anyway, the factory compiles again. We still got this part of the Statement left:
1 fleet.Pick(trainId).Returns(train);
2
3 //WHEN
4 command.Execute();
5
6 //THEN
7 Received.InOrder(() =>
8 {
9 train.ReserveSeats(seatCount, reservationInProgress);
10 fleet.UpdateInformationAbout(train);
11 };
Johnny: That’s just introducing three methods. You can handle it.
Benjamin: Thanks. The first line is fleet.Pick(trainId).Returns(train). I’ll just generate the Pick method using my IDE:
1 public interface TrainFleet
2 {
3 ReservableTrain Pick(string trainId);
4 }
The TrainFleet interface is implemented by the TodoTrainFleet we talked about several minutes ago. It needs to implement the Pick method as well or else it won’t compile:
1 public class TodoTrainFleet : TrainFleet
2 {
3 public ReservableTrain Pick(string trainId)
4 {
5 throw new NotImplementedException();
6 }
7 }
This NotImplementedException will land on our TODO list just as you mentioned. Nice!
Then comes the next line from the Statement: train.ReserveSeats(seatCount, reservationInProgress) and I’ll generate a method signature out of it the same as from the previous line.
1 public interface ReservableTrain
2 {
3 void ReserveSeats(uint seatCount, ReservationInProgress reservationInProgress);
4 }
ReservableTrain interface doesn’t have any implementations so far, so nothing more to do with this method.
The last line: fleet.UpdateInformationAbout(train) which needs to be added to the TrainFleet interface:
1 public interface TrainFleet
2 {
3 ReservableTrain Pick(string trainId);
4 void UpdateInformationAbout(ReservableTrain train);
5 }
Also, we need to define this method in the TodoTrainFleet class:
1 public class TodoTrainFleet : TrainFleet
2 {
3 public ReservableTrain Pick(string trainId)
4 {
5 throw new NotImplementedException();
6 }
7
8 void UpdateInformationAbout(ReservableTrain train)
9 {
10 throw new NotImplementedException();
11 }
12 }
Johnny: This NotImplementedException will be added to the TODO list as well, so we can revisit it later. It looks like the Statement compiles and, as expected, is false, but not for the right reason.
Benjamin: Let me see… yes, a NotImplementedException is thrown from the command’s Execute() method.
Johnny: Let’s get rid of it.
Benjamin: Sure. I removed the throw and the method is empty now:
1 public void Execute()
2 {
3
4 }
The Statement is false now because the expected calls are not matched.
Johnny: Which means we are finally ready to code some behavior into the NewReservationCommand class. First, let’s assign all the constructor parameters to fields – we’re going to need them.
Benjamin: Here:
1 public class NewReservationCommand : ReservationCommand
2 {
3 private readonly string _trainId;
4 private readonly uint _seatCount;
5 private readonly TraingFleet _fleet;
6 private readonly ReservationInProgress _reservationInProgress;
7
8 public NewReservationCommand(
9 string trainId,
10 uint seatCount,
11 TrainFleet fleet,
12 ReservationInProgress reservationInProgress)
13 {
14 _trainId = trainId;
15 _seatCount = seatCount;
16 _fleet = fleet;
17 _reservationInProgress = reservationInProgress;
18 }
19
20 public void Execute()
21 {
22 throw new NotImplementedException();
23 }
24 }
Johnny: Now, let’s add the calls expected in the Statement, but in the opposite order.
Benjamin: To make sure the order is asserted correctly in the Statement?
Johnny: Exactly.
Benjamin: Ok.
1 public void Execute()
2 {
3 var train = _fleet.Pick(_trainId);
4 _fleet.UpdateInformationAbout(train);
5 train.ReserveSeats(seatCount);
6 }
The Statement is still false, this time because of the wrong call order. Now that we have confirmed that we need to make the calls in the right order, I suspect you want me to reverse it, so…
1 public void Execute()
2 {
3 var train = _fleet.Pick(_trainId);
4 train.ReserveSeats(seatCount, reservationInProgress);
5 _fleet.UpdateInformationAbout(train);
6 }
Johnny: Exactly. The Statement is now true. Congratulations!
Benjamin: Now that I look at this code, it’s not protected from any kind of exceptions that might be thrown from either the _fleet or the train.
Johnny: Add that to the TODO list - we will have to take care of that, sooner or later. For now, let’s take a break.
Summary
In this chapter, Johnny and Benjamin used interface discovery again. They used some technical and some domain-related reasons to create a need for new abstractions and design their communication protocols. These abstractions were then pulled into the Statement.
Remember Johnny and Benjamin extended effort when test-driving the controller. This effort paid off now - they were free to shape abstractions mostly outside the constraints imposed by a specific framework.
This chapter does not have a retrospective companion chapter like the previous ones. Most of the interesting stuff that happened here was already explained earlier.