How To Write Tests

How To Write Tests

Testing style guide for rails grade

Four-phase test

Use clear, four-phase tests.

Tests should be readable in isolation

Students should be able to click "Examine Test" and read and understand a test easily. Therefore, each test needs to be self-contained.

Therefore,

Avoid before, let

Let's avoid mystery guests.

Use a describe block with a target URL and a single descriptive test nested inside

Bad

context "with input 5" do
  it "works", points: 2, hint: h("params_are_strings") do
    visit "/flexible/square/5"

    expect(page).to have_content(25)
  end
end

Good

Avoid deeply nesting describe/feature, context, it/scenario

Bad

Better

Best

This also allows for tailoring copy more instructively to each individual spec, rather than shoehorning it to fit a DRYer structure.

Test features progressively to break a problem into bite-size steps

Add tests for the presence of hardcoded copy or inputs/labels before testing for behavior. This approach lets students better narrow down what they're missing in their code.

Bad

Good

One expectation per test

Try to stick to one expectation per test, except when absolutely necessary (e.g. to ensure proper setup).

Ref:

https://devblast.com/b/ruby-testing-with-rspec-one-expectation-per-test

Customized Failure Messages

Use customized failure messages for any expects whose messages are not 100% self-explanatory.

Ref:

https://relishapp.com/rspec/rspec-expectations/docs/customized-message

Hints

You can add hints to each spec to give students just-in-time help even when we aren't standing next time them. This is next-level rubber-ducky stuff.

For example,

What's going on above:

  • You can provide additional help to the student by optionally including a hint with the it or scenario method. The value should be a string or array of strings.

  • Keep hints in I18n so that they don't clutter up the readability of tests, and so that you can easily add the same hint to multiple relevant tests:

  • Store hints under keys under en.hints.

  • You can use GitHub-flavored Markdown.

  • To further reduce clutter, there is a helper method h() in spec_helper.rb which makes it easy to add multiple hints from I18n:

    So you can just provide a single string with the I18n keys of multiple hints separated by spaces:

Use factories

I go back and forth over factories vs just ActiveRecord objects, since students know exactly what ActiveRecord objects are.

Currently I lean towards using factories since, used right, they produce minimally valid objects out-of-the-box, and so save so much code.

More important than brevity, however, is that you can then define or re-define only the attributes that are important to the test at hand, thereby drawing attention to them.

I think create(:photo) is intuitive enough for students to guess what it means; it's no more magical to them than the rest of the test code (Capybara methods, etc) that they aren't being explicitly taught.

Capybara selectors

  • tried has_content: too much noise

  • tried has_css with id: unfamiliar (we could introduce it)

  • tried attribute selector, e.g. [data-grade="occurrences"]; too weird looking

  • settled on expect(page).to have_css(".occurrences", text: 2) for now since it is very familiar to them, but it feels like class is something that is used for too many other things.

Perhaps teaching id, label, for, and styling with # earlier so that we can select with it might be worthwhile.

When stubbing external requests

Testing external requests is tricky since forcing capybara to wait for a response will have inconsistent results. Therefore, it's best to stub external requests and control the response in our test suite.

Typically, stubbed requests are added in one of the test helper files (either spec/spec_helper.rb or spec/rails_helper.rb ) within an Rspec.configure block.

Stubbed request should be as flexible and forgiving as possible. Use regexp to:

  • Allow for http and https

  • Allow for mixed case

  • Allow for dynamic url segments

  • Allow for arbitrary additional query string parameters not specified by us (for example, access tokens)

WIP notes below

http://stackoverflow.com/questions/11377087/can-i-use-capybara-rspec-to-match-a-range

Stubbed requests: Perhaps we create a method for each stubbed request and then call that method at the beginning of the test. Would that be clearer to the student?


2022 Notes

HTML/CSS Specs

Prefer Capybara finder methods for checking for HTML elements, content, and attributes since they provide the most clarity and flexibility when writing tests.

The most useful methods are all and find.

Note: find will throw an error if it doesn't find anything. The error message is readable enough for students, but you can still customize it in the expect assertion if you want.

Testing for element

Other ways to write this test:

using rspec-html-matchers

regular capybara

Testing for an element with specific content

or

Testing HTML attributes

You can retrieve values from attributes if you select an element with find or all.

Use the draft_matchers gem for testing CSS color (by color name), page layout, or element ancestry.

Note the js: true is required for testing layout.

Testing Copy (in a webpage)

When testing copy and not data that's been created:

  • Ignore case

  • Ignore punctuation

  • Using Regex for this is okay 👌

    • note that extraneous spaces between words should also be okay, since the rendered HTML page will collapse it all down to one space in the end.

Ruby Specs

When writing tests that check for specific output in the Terminal, define this helper in spec_helper.rb, rails_helper.rb, or the spec file.

Use the method along with the path of the script file that is being tested like this:

Use Regex when testing the output:

Make sure the Regex will match the expected output regardless of which printing method a student uses.

You can customize the error message too.

note: whitespace is respected in the build report in Grades, so add to space out the expected vs actual output on their own lines.

Stubbing method calls

If the script is testing a method that that involves randomness, user input, or time you stub the method call and choose something specific for the test case.

For instance methods

For class methods

Notes about stubbing gets

Make sure to add to end of the argument to the and_return method since :gets always returns it. If you don't, sometimes weird errors can happen if students use methods we don't expect (like chop instead of chomp).

One full spec might look like this:

Preventing hardcoded solutions from passing

It would be fairly difficult to prevent all forms of hardcoding for a solution, but preventing the basic "print expected result" is possible by examining the contents of the script file (make sure you ignore commented lines).

Writing multiple tests for the same script

When writing multiple tests for the same script you need to un-require the script in each test first, so that it can be successfully required and the output captured no matter what order the specs are run in. You can un-require a script by using the global Ruby variable $".

Testing that a method has been defined

You can use method_defined? to check that an instance method or attr_accessor has been defined.

Rails Specs

Routes

Testing that a route is visit-able

if a route should redirect...

Forms, params

Write very incremental tests. You should write a test for each of the following:

  • form element

  • form action

  • label with text

  • count for specific input type (text input, textarea, file input)

    you can also use find here with the type attribute.

  • button with text

  • form submits to a real page

  • specific input has name attribute

  • label connected to input

  • form submits and produces correct content

Using find and set for input elements allow for the most flexibility. You can make label text case insensitive and determine what the for and id attributes are set to (if at all) and display them in the error message.

Database / ActiveRecord

Testing that a class/Model has been defined

Testing that a database table has been created

Testing that an instance method has been defined

Testing columns names and data types

Testing association accessors

Use the shoulda-matchers gem. Ensure the project is setup to work with shoulda-matchers.

Note: Watch out for the required: true option for 1-N.

Testing with Time

If you're writing tests that relate to time, you can include active support time helpers in rails_helper.rb

Make sure you use Time.current or Date.current instead of Time.now for weird time zone inconsistencies.

Example:

Last updated