Temporary State in Tests: Bloc(k) Party
Tags: ruby, rails
Published: 2021-08-16 12:14:09

## Testing Around the Clock ([Sunday](https://youtu.be/vEqIWT9EjDY))

Tests will sometimes require us to bend the truth and modify the state of the world in order to have repeatable, consistent results and feedback.

We're building an application that sends a notification on Sunday. In order to test it sending a notification, we need it to be Sunday. The problem with that is I don't really like working on Sundays.

If I'm writing code on Wednesday, I want to make sure I don't break notifications. Rather than waiting until Sunday to run my tests, I can trick my test into thinking it's Sunday. Rails has built-in [helpers](https://api.rubyonrails.org/v5.2.6/classes/ActiveSupport/Testing/TimeHelpers.html) we can use to simulate that.

```ruby
travel_to Time.zone.local(2021, 8, 1, 1, 2, 34)
notifier = Notifier.new
expect(notifier.send_notification?).to eq true
```

When we do this, we want to ensure that the results are _temporary_. Other tests, or even areas within a given test, shouldn't be affected by this state change. It should still be whatever day the tests are **really** run otherwise.

Now, in the case of these helpers from Rails, according to the documentation:

> The stubs are automatically removed at the end of the test.

However, we want to be clear to readers who may not have perused the Rails documentation (nor have encyclopedic knowledge of it). Additionally, we may only want part of the test to think it's Sunday. In order to isolate this change, we can limit its impact to be within a block.

```ruby
notifier = Notifier.new
travel_to Time.zone.local(2021, 8, 1, 1, 2, 34) do
  expect(notifier.send_notification?).to eq true
end

travel_to Time.zone.local(2021, 8, 2, 1, 2, 34) do
  expect(notifier.send_notification?).to eq false
end
```

## Benefits of the Block ([Little Thoughts](https://youtu.be/q6cS26oEX64))

The block helps visually distinguish the scope of the change, in addition to ensuring that the change is limited to the code within the block. I find it to be more readable and clear using a block. We have temporarily changed the state of the world, but only in a limited capacity.

As far as the test is concerned, it's Sunday in the first block, Monday in the second block, and whatever day of the week you're reading this before and after the blocks.

## Disregarding Bullet Recommendations ([Real Talk](https://youtu.be/mmMmoRiAoOc))

If you've used [Bullet](https://github.com/flyerhzm/bullet) before for eager loading optimization, you may have run into situations in which you wished it wouldn't tell you something either needed or didn't need to be eagerly loaded.

Now, some times that's the tool doing its job: giving you feedback you may not have expected (or wanted). However, in tests, it may be giving you a false positive because of your test setup.

In a test that calls a method where only one record exists in your database, Bullet may suggest that you don't need the eager loading. And Bullet is correct - in this case, for this test. However, the setup in that test may not reflect reality or the expectation most of the time. It's much more likely that there are **many** records, which will cause Bullet to recommend the `includes` were you to get rid of it.

How can we keep our test and satisfy Bullet? We don't; instead, let's turn Bullet off. But, we don't want Bullet to stay off for the rest of the test run. We only want it off when calling that one method in this one test.

If you're using RSpec, you can use tags to [turn Bullet off](https://tosbourn.com/ignore-bullet-rspec/) for an entire test, file, or context. It's a clever and succinct approach.

This is a post about blocks, so let's discuss another way.

## Turn It Off And On Again ([Flux](https://youtu.be/ttcboE1GrNg))

We've discussed how to **use** a method that takes a block to temporarily change state in our application, specifically tests. Let's start by writing the code we want:

```ruby
it "doesn't complain about Bullet warnings" do
  disable_bullet do
    expect(Notification.method_that_eager_loads_associations.size).to eq 1
  end
end
```

Let's write an implementation of `disable_bullet`. We can use `yield` to wrap turning Bullet off and on again inside of `disable_bullet`:

```ruby
def disable_bullet
  Bullet.enable = false
  yield
  Bullet.enable = true
end
```

Calling this method turns Bullet off, runs whatever code is in the block passed to it, and then turns Bullet back on after the block completes. We can make the block optional by calling the `block_given?` method:

```ruby
def disable_bullet
  Bullet.enable = false
  yield if block_given?
  Bullet.enable = true
end
```

However, we're going to remove that in this case because there's no use case for calling this method otherwise. We'll get an error without passing a block to `disable_bullet`, and that's ok.

## Why Did I Do This Again? ([I Still Remember](https://youtu.be/5R-9IgWD36A))

When you're writing the test that you need to disable Bullet, you likely have the context of why Bullet needed to be turned off. However, when you're reading it later, you may forget. And maybe someone else wrote this test and you don't know to begin with. Is it _reasonable_ to have turned Bullet off? We can force this to be documented in the test by making an addition to our method.

```ruby
def disable_bullet(reason:)
  Bullet.enable = false
  Bullet.enable = true
end
```

Callers are now required to pass in a **reason** why Bullet has been turned off. It's not used in the method, but it helps enforce a thoughtful understanding of why this tool is being turned off when the method is called.

```ruby
it "doesn't complain about Bullet warnings" do
  disable_bullet(reason: "Trust me. But seriously, here's why...") do
    expect(Notification.method_that_eager_loads_associations.size).to eq 1
  end
end
```

By using a block here, we have provided a way to _temporarily_ change the state of the world with an aesthetic that clearly denotes the scope of the change, and we also added a way to force users to justify their use of the method to themselves and others.

## Inspiration ([Pioneers](https://youtu.be/JrpMIkaP2ss))

Thanks to [Rob Whittaker](https://twitter.com/purinkle) for [asking me](https://twitter.com/purinkle/status/1412071536819851268) about other ways I like to use blocks, based on an [earlier post](https://dev.to/thegnarco/activerecord-s-new-takes-a-block-kid-jge).

> This post originally published on [The Gnar Company blog](https://blog.thegnar.co/temporary-state-in-tests).

_Learn more about how The Gnar [builds Ruby on Rails applications](https://www.thegnar.com/software-development/ruby-on-rails-development)._