Follow @RoyOsherove on Twitter

Unit Testing Entry and Exit Points

Let’s talk about the word “unit” in unit testing.  To me, a unit stands for “unit of work” or a “use case” inside the system. A unit of work has a beginning and an end. I call these entry points and exit points. A simple example of a unit of work, as we’ll soon see, is a function that calculates something and returns a value. But that function could also use other functions , other modules and other components in the calculation process, which means the unit of work (from entry point to exit point), now spans more than just a function.

Yes that also means you can apply the term ‘unit of work’ to higher level abstractions, as well as use these terms when discussing integration, API and end-to-end tests. We’re focusing on the unit testing side.

Definition : Unit of Work

A unit of work is the sum of actions that take place between the invocation of an entry point up until a noticeable end result through one or more exit points. The entry point is the thing we trigger. Given a publicly visible function, for example:

The function’s body is all or part of the unit of work.

The function’s declaration and signature are the entry point into the body.

The resulting outputs or behaviors of the function are its exit points.  

Entry Points & Exit Points

A unit of work always has an entry point and one or more exit points.

It might be useful to give a quick real-world example, or use case, of what you mean by a unit of work. It seems to me the term could span several different components, and it might be helpful to ground it in a real-world concept.

unit-of-work1.png

A unit of work can be a single function, multiple functions, multiple functions or even multiple modules or components. But it always has an entry point which we can trigger from the outside (via tests or other production code), and it always ends up doing something useful. If it doesn’t do anything useful, we might as well remove it from our codebase.

 

What’s useful? Something publicly noticeable happens in the code: A return value, state change or calling an external party. Those noticeable behaviors are what I call exit points. Listing 1.1 will show a simple version of a unit of work.

unit-of-work2.png



Why “exit point”?

Why “exit point” and not something like “behavior”?. My thinking is that behaviors can also be purely internal. We’re looking for externally visible behaviors from the caller. That distinction might be difficult to differentiate during “live action” coding. Also, “exit point” has the nice connotation to it that suggests we are leaving the context of a unit of work and going back to the test context. Behaviors might be a bit more fluid than that. That said, I’m not sure. Perhaps this will change in the 4th edition of my art of unit testing book…

Here’s a quick code example of a simple unit of work.

const sum = (numbers) => {
    const [a, b] = numbers.split(',');  
    const result = Number.parseInt(a, 10) + Number.parseInt(b, 10);  
    return result;
};

This unit of work is completely encompassed in a single function. The function is both the entry point, and because its end result returns a value, it also acts as the exit point. We get the end result in the same place we trigger the unit of work, so we can describe it such that the entry point is also the exit point.
If we drew this function as a unit of work, it would look something like this:

unit-of-work1.1.png

I used “sum(numbers)” as the entry point and not “numbers” because the entry point is the function signature. The parameters are the context or input given through the entry point. 

Here’s a variation of this idea:

A Unit of work has entry points and exit points:

let total = 0;
const totalSoFar = () => {
  return total;
};

const sum = (numbers) => {
  const [a, b] = numbers.split(',');
  const result = Number.parseInt(a, 10) +
            Number.parseInt(b, 10);
  total += result;
  return result;
};

module.exports = {
  sum,
  totalSoFar
};

This new version of sum has two exit points. It does two things:

1.     It returns a value

2.     it has new functionality: it has a running total of all the sums. It sets the state of the module in a way that is noticeable (via totalSoFar() ) from the caller of the entry point.

unit-of-work1.2.png

You can think of these two exit points as two different paths, or requirements from the same unit of work, because they indeed are two different useful things the code is expected to do.

It also means I’d be very likely to write two different unit tests here: one for each exit point. Very soon we’ll do exactly that.

What about totalSoFar()? Is this also an entry point? Yes, it could be. IN a separate test. I could write a test that proves that calling totalSoFar() without triggering prior to that call returns 0. Which would make it its own little unit of work. And would be perfectly fine. Often one unit of work (sum()) can be made of smaller units. This is where we can start to see how the scope of our tests can change and mutate, but we can still define it with entry points and exit points.

Entry points are always where the test triggers the unit of work. You can have multiple entry points into a unit of work, each used by a different set of tests.

A side note on design

In terms of design, you can also think of it like this. There are two main types of actions: “Query” actions, and “Command” functions. Query actions don’t change stuff, they just return values. Command actions change stuff but don’t return values.

We often combine the two but there are many cases where separating them might be a better design choice. This post isn’t primarily about design, but I urge you to read more about the concept of command-query separation over at Martin Fowler’s website: https://martinfowler.com/bliki/CommandQuerySeparation.html  

Exit points often signify requirements and new tests, and vice versa

Exit points are end results of a unit of work.  For Unit Tests, I usually write at least one separate test, with its own readable name, for each exit point. I could then add more tests with variations on inputs, all using the same entry point, to gain more confidence.

Integration tests, on the other hand, will usually include multiple end results since it could be impossible to separate code paths at those levels. That’s also one of the reasons integration tests are harder to debug, get up and running, and maintain: they do much more than unit tests, as we’ll soon see.

Back to the code:

Here’s a third version of this function :

const winston = require('winston');
let total = 0;

const totalSoFar = () => {
  return total;
};

 
const makeLogger = () => {
  return winston
    .createLogger({
      level: 'info',
      transports: new winston.transports.Console()
    });
};

const logger = makeLogger();
const sum = (numbers) => {
  const [a, b] = numbers.split(',');
  logger.info(
    'this is a very important log output',
    { firstNumWas: a, secondNumWas: b });
  const result = Number.parseInt(a, 10) + Number.parseInt(b, 10);
  total += result;
  return result;
};

module.exports = {
  totalSoFar,
  sum
};

 You can see there’s a new exit point/requirement/end result in the function. It logs something to an external entity. It could be a file, or the console, or a database. We don’t know, and we don’t care.

This is the third type of an exit point: calling a third party. I also like to call it “calling a dependency”.

A Dependency (3rd party)

A dependency is something we don’t have full control over during a unit test. Or something that, to try to control in a test, would make our lives miserable to get up and running, maintain, keep the test consistent, or running fast. Some examples would include: loggers that write to files, things that talk to the network, code controlled by other teams that we cannot change, components that due to the way they function take a very long time (calculations, threads, database access) and more.  The rule of thumb is: “If I can fully and easily control what its doing, and it runs in memory, and its fast, it’s not a dependency”. There are always exceptions to the rule, but this should get you through 80% of the cases at least.


Here's how I’d draw it with all three exit points, and two entry points together:

unit-of-work3.png

At this point we’re still just a function-sized unit of work. The entry point is the function call, but now we have three possible paths, or exit points, that do something useful that the caller can verify publicly.

Test Per Exit Point

Here’s where it gets interesting: It’s a good idea to have at least one separate test per exit point. (it might have more than a single assert, but only on the things related to exit point, that would be changing together. This will make the tests more readable, simpler and easier to debug or change without affecting other outcomes.

Exit Point Types

We’ve seen that we have three different types of end results:

Value Based

§  The invoked function returns a useful value (not undefined). If this was in a more static language such as Java or C# we’d say it is a public, non void function.

State Based

§  There’s a noticeable change to the state or behavior of the system before and after invocation that can be determined without interrogating private state. (In our case the wasCalled() function returns a different value after the state change.)

3rd Party

§  There’s a callout to a third-party system over which the test has no control. That third-party system doesn’t return any value, or that value is ignored. (Example: calling a third-party logging system that was not written by you and you don’t control its source code.)

XUnit Test Patterns’ Definition of Entry & Exit Points

The book XUnit Test Patterns discusses the notion of direct inputs and outputs,  and indirect inputs and outputs. Direct inputs  are what I like to call entry points. That book called it “using the front door” of a component. Indirect outputs in that book can be thought of as the other two types of exit points I mentioned (state change and calling a third party).  Both versions of these ideas have evolved in parallel, but the idea of “unit of work” only appears in this book. Unit of work , coupled with “entry” and “exit” points makes much more sense to me that using “direct and indirect inputs and outputs”. Consider this a stylistic choice on how to teach the concept of test scope. You can find more about xunit test patterns at xunitpatterns.com  

Let’s see how the idea of entry and exit points affects the definition of a unit test.

UPDATED DEFINITION of a unit test:

A unit test is a piece of code that invokes a unit of work and checks one specific exit point as an end result of that unit of work. If the assumptions on the end result turn out to be wrong, the unit test has failed. A unit test’s scope can span as little as a function or as much as multiple modules or components depending on how many functions and modules are used between the entry point and the exit point. 

1.2          Different Exit Points, Different Techniques

Why am I spending so much time just talking about types of exit points? Because not only is it a great idea to separate the tests per exit point, but also each type of exit point might require a different technique to test successfully.

·       Return-value based exit points (direct outputs per “xunit patterns”) of all the exit point types, should be the easiest to test. You trigger an entry point, you get something back, you check the value you get back.

·       State based tests (indirect outputs) require a little more gymnastics usually. You call something, then you do another call to check something else (or call the previous thing again) to see if everything went according to plan.

In a third-party situation (indirect outputs) we have the most hoops to jump through. We haven’t discussed the idea yet, that’s where we’re forced to use things like mock objects in our tests so we can replace the external system with one we can control and interrogate in our tests. I’ll cover this idea deeply later in the book.

Which exit points make the most problems?

As a rule of thumb, I try to keep most of my tests either return value based or state based tests. I try to avoid mock-object based tests if I can, and usually I can. As a result I usually have no more than perhaps 5% of my tests using mock objects for verification. Those types of tests complicate things and make maintainability more difficult. Sometimes there’s no escape though, and we’ll discuss them in future posts. 

COVID19: All current online unit testing and TDD training content is free until further notice

Re-Thinking the Role of Mock Objects, Design & Test Maintainability (stream of thought)