RSpec: Minimum Valid Object (MVO)

Adam + Cuppy
RSpec Design Patterns
7 min readJan 12, 2019

A well-structured test suite should instill confidence. If you can’t trust that your test suite is running scenarios that confirm your implementation is running as expected, then it’s only a matter of time before confidence team-wide begins to fall.

Minimum Valid Object (MVO) is a unit testing pattern that starts by confirming the object under test (the “subject”) is in a valid state prior to modification. So, if you’re using Ruby on Rails or another framework that has model validations, such as, email address format or first-/lastname length, then confirming that you’re working with a valid object at the start is critical before you start testing that the object is invalid due to those requirements not being fulfilled.

Here’s is a basic Userobject with two validations:

Here’s an example

From what it appears, we’re testing the firstname attribute’s validations. We’re using a before block to set a @user instance variable (our implied “subject”) and running our assertions against it — simple enough.

When we run our tests and it passes — yay!

But, alas! It’s nothing but lies. You see while the test are passing, the tests are actually faulty, and are giving us a false positive. Let’s take a closer look…

Our test is expecting the object to be invalid based on the firstname attribute being too long, too short or if it includes symbols, but that’s not the entire reason it’s failing. When we look closer…

  1. The character string is not actually 50+ characters long (like our validation requires and we believe our setup is establishing)
  2. What makes the object invalid is not just the firstname, it’s the absence of the lastname attribute (and resulting validations).

And… this is where we start to lose confidence in our test suite— How many other times did we create a false positive?

Communication and isolation are our friends

I don’t know about you, but I can’t really tell, after a quick scan, if TOOOOOOOOOOLOOOOOOOONNNNNNNGGGGGG is 50 characters long or not, so we count them… every time (it’s 33 by the way). Also, there’s a chance that at one point, the limit may have been 30 characters (3 less than this string), but we made the change to the implementation (to a maximum of 50) and the test still passed (due to the reasons I mentioned above).

Yes, we’re smart enough to count. And yes, we’re smart enough to add a few characters to the string length, but you know what, we would be missing a glaring issue: The test is brittle. Not in the sense that it creates a false positive, but because it takes manual processing to understand what’s wrong and why, when we could have easily prevented it in the first place.

Remember that time, not too long ago, when you were just learning Ruby or Rails or Java or PHP and you were trying to interpret simple errors in the code. Is it a missing semicolon?! Is it a line break?! WTF?!?!?! Then, as time passed you were an error handling ninja! Oh, we’re not in the right namespace — duh! Oh, the load order is backward and we’re not bound to the right context — duh!

We’ve all been there!

As a team, we communicate through our code — intention and expected outcome. If we’re not doing that, then we’re selfishly creating art for art's sake. Be communicative! And, this is a method to do that through your tests…

Don’t sacrifice communication for speed

I get it: we want our test suite to run fast! However, speed is all for not if we introduce a false positive and create more development time on the frontend.

With communication and clear expectations in mind, let’s dive in…

The RSpec DSL can often confuse that we’re dealing with plain/jane Ruby. We can define helper methods, classes, etc. either within an example block or outside the test entirely. Now, I don’t encourage excessive use of helper methods or additional logic in the tests themselves; however, if we find that we’re duplicating test assertions or manually creating conditions that may be variable (e.g., name strings), a helper is a great one-time investment that pays out over time.

Step 1: Add a helper to generate a name string

Our new helper takes one argument (length). This simple method will now return a random string of alpha characters. Before we add it, we can run an IRB session to confirm the helper works as expected, then we leave it alone. Could we pass in a collection of characters? Sure. Should we? Maybe. The real question is: Are we creating a domain knowledge dependency? Meaning: What does an engineer now need to understand about the tests themselves to be able to work with them and interpret the results? If the answer is anything more than a couple of minutes, then back down the complexity.

Step 2: Specify exactly what’s changing

Next, we update the example description to reflect a specific modification (change “Firstname is too long” to “Firstname is over 50 characters”) and replace instance variables with the use of let. (This goes into a much larger debate around let versus @instance variables, but you can find more about why you should use let in this post Let me use you).

While we might squeeze out a millisecond or two by not setting up another variable (excessive_length) we’re not going for compilation speed! We’re going for clear communication — What does this value represent?

Also, you’ll notice that instead of using 51 as a value for the length of the string, we’ve used 50 + 1. Yes, we could use 51 as the value of excessive_length, but by expressing it as 50 + 1 we’re communicating that 50 is relevant and that we’re intentionally increasing that number by 1, which outlines the intention of our test. And, as an added benefit…

If in our implementation, we increase the maximum length from 50 to 60 (or whatever) we can now grep for “50” and replace it with “60” and our tests are usable.

When we apply this step to the rest of our test, we get this:

What have we done so far?

  • Used generate_name to build name strings based on a supplied character length (creating uniformity across all tests relating to name strings and length)
  • Explicitly described what modifications are being tested by clarifying the example description; and…
  • Used named let to define values throughout the test examples

Our next step is to DRY’up (Do Not Repeat Yourself) our test code…

First, move the common used let(:user) to the top of the file.

Second, leverage let(:user) in conjunction withlet(:firstname) to define a new firstname within each example block. (If you’re new to RSpec, let declarations cascade down through nested describe and context blocks. That means that if you define let(:firstname) in one block, and redefine it in a nested block, it will overwrite the first with the second.)

Our example blocks only contain only what’s relevant: String length, firstname attribute and the assertion — yay!

What remains? it { expect(user).to_not be_valid } How do we handle that? For now, we don’t. That’s right, we leave it. Why?! Isn’t it redundant? Yeah, it is. But, for now, the tests tell a story…

When firstname is excessive/insufficient/improperly the user is not valid.

We can simplify our test implementation with an implied object under test with subject and replace let(:user) with subject(:user) and now in place of it { expect(user).to_not be_valid } we can call it { is_expected.to_not be_valid }. A small change, but provides readability.

Although RSpec allow you to use subject in place of the named value user, I do not recommend it. While it is the subject under test, it doesn’t specify what type of object the subject is.

Our test would now look like this:

Whichever your team feels better communicates the intention — subject or let — is up to you. I use both.

While our test is much cleaner, we have yet to implement the MVO design pattern. What’s missing? The initial valid state. We have to confirm that the subject is valid before we start making changes; then we know that our modification is the problem. Everything to this point is set up.

First: Move/copy all modifiable attributes to let declarations at the top of the uppermost describe block. For each let attribute, declare an acceptable value.

Second: Before our first it tests each nested describe blocks, we run our key assertion: it { is_expected.to be_valid }

Now, if that assertion passes, we know that we’re starting with a valid object. And, can safely assume that every change we make, which causes an invalid state, is isolated to a nested example block — Win!

What happens when we run our test? It fails! Why? Well, if we look back at our implementation, there’s a requirement for a lastname attribute. So, let’s make the change:

If we change our implementation and redefine what a valid User object would be, we catch it before we start receiving false positives.

The Minimum Valid Object (MVO) unit testing pattern has two core principles:

  1. Establish a valid object with valid attributes up front
  2. Make one modification at a time and assert that that change makes the object invalid

Have a question about one of your test?

I’m always willing to help. Find me on github.com and email me for a totally free 30-minute pair session. Let’s solve some problems together!

Adam Cuppy is a member of the World’s Most Zealous web and mobile application development company, Zeal. We empower product owners and engineering teams to development software that’s elegant, well structured and objective driven. We specialize in Ruby on Rails, React.js and React-Native (iOS & Android).

Get Zealous at codingzeal.com

--

--

Adam + Cuppy
RSpec Design Patterns

I’m not a best selling author, not a Fortune 500 CEO, not a Nobel Prize recipient, but I speak around the world on confidence in software development.