---
title: Testing Your Edge Cases
teaser: A little combination math goes a long way to catching edge cases.
tags: testing,ruby,development
author: Joël Quenneville
published_on: 2021-09-08
---

We have just added some new permissions logic to a project.

```ruby
def can_read?
  article.access_policy&.public? || user.admin?
end
```

We might include some tests that read like:

```ruby
describe "#can_read?" do
  it "allows admin to access public article"
  it "allows admin to access private article"
  it "allows non-admin to access public article"
  it "disallows non-admin from accessing private article"
end
```

During code review, one of our colleagues asks

> Do we have any tests that cover the `nil` case?

Hmmm, it looks like we forgot! How can we do a better job of making sure we
cover all our edge cases?

## Using math

We can use math to calculate how many combinations of inputs we have. In order
to do so, we need to identify each of the inputs that could affect our method,
calculate how many different states they can have, and multiply them together.

Here, two variables influence the outcome of our method: the
access policy and the user's role. They can be in the following number of
states:

1. An article's public status can be in `3` states ([nil, true, or false])
2. A user being an admin can have `2` states (true or false)

Multiplying these gives us `6` possible combinations of inputs. We probably want
6 unit tests to cover all the combinations but our original implementation only
had 4. Time to add the missing 2 tests to cover `nil`!

[nil, true, or false]: https://thoughtbot.com/blog/avoid-the-threestate-boolean-problem

## Visually

Sometimes we may prefer to calculate the different cases visually using a truth
table or [decision table]. Taking this approach is more explicit but doesn't
scale up well to many inputs or many states due to **combinatorial explosion**.

| access  | role  | can edit |
|---------|-------|----------|
| public  | admin | true     |
| public  | other | true     |
| private | admin | true     |
| private | other | false    |
| nil     | admin | true     |
| nil     | other | false    |

[decision table]: https://www.hillelwayne.com/decision-tables

## More than just booleans

Calculating combinations is useful for more than just booleans. Consider the
following constructor for a query object:

```ruby
class ArticleQuery
  def initialize(limit: nil, scope: Article.all)
    # ...
  end
end
```

There are at least 4 different ways of invoking this method (2 arguments, each
of which can be explicitly passed or defaulted). We probably should have a test
for each of these cases.

1. `new`
2. `new(limit: 10)`
3. `new(scope: Article.published)`
4. `new(limit: 10, scope: Article.published)`

There may be even more cases. Sometimes explicitly passing `nil` may be
different than not passing anything. The `scope` argument could have 3 states
(default, explicit scope, explicit `nil`).

Some types of values like strings or arrays have theoretically infinite states.
In practice we probably care about a finite number of slices of that infinite
domain. For example, our code might only distinguish between empty, single, and
multi-item arrays giving us functionally 3 different states.

## Reducing the states

In our original example, we identified 2 edge cases that weren't covered in our
test suite. One solution is to **add some tests** so that our suite now covers
the 6 scenarios handled by our code.

Alternatively, we could **reduce our code** to only handle the 4 scenarios
described in our test suite. Testing pain is often a sign that we should change
our implementation.

## Pushing uncertainty to the edges

Nils in particular cause a [lot of edge cases] and [pushing them to the edges of
our system] as much as possible will make both our coding and testing lives
easier.

In our original example, we may decide to make access policies required on
articles, eliminating the potential `nil` and bringing us down to 4 possible
combinations.

Similarly, we might remove the defaults from the `ArticleQuery` constructor. Now
we have eliminated a lot of uncertainty as there is only one way to invoke it. A
win for both our code and our test suite!

[lot of edge cases]: https://thoughtbot.com/blog/if-you-gaze-into-nil-nil-gazes-also-into-you
[pushing them to the edges of our system]: https://avdi.codes/self-confident-code
