Intro to Testing
Learning Goals
- Understand why we use tests
- Define the stages of a test
- Define a RSpec test
- Use a variety of assertion methods
Vocabulary
- Gem
- Test
- Assertion
Warm Up
- Thinking on your work so far, how did you know if your code/program was working?
- What are some potential drawbacks to this approach?
Test Etiquette
File Structure
- Spec files live in their own
spec
directory - Implementation code files live in a sibling
lib
directory - Spec files should reflect the class they’re testing with
_spec
appended to the file name, e.g.spec/name_of_class_spec.rb
- In your test, you’ll now
require "./lib/name_of_class.rb"
- Run your spec files from the root of the project directory, e.g.
rspec spec
- If you want to run a specific spec file you can append the location of that file to the
rspec spec
command. Sorspec spec spec/name_of_file_spec.rb
.
├── lib
| └── name_of_class.rb
└── spec
└── name_of_class_spec.rb
RSpec Setup
RSpec is a framework used for automated testing. It is the testing framework used on many of the homework exercises you’ve been assigned. RSpec Core Documentation
gem install rspec
- Require
rspec
- the easy and explicit way to run all your tests
RSpec Convention
- At the top of every spec file:
describe NameOfClass
- describe ‘#name_of_method’
- It is good practice to have another describe block for the name of method. That way we can group all assertions dealing with this method in this describe block.
- We need to have an assertion at the end of every test
- A lot of times we are going to compare if two values are equal to each other
- We do that by writing
expect(actual).to eq(expected)
where actual is the result of the method call or object querying, and expected is the value we expect it to be.
- RSpec Expectations Documentation
Code-Along
Files for our code along can be found in the se-mod1-exercises repo
Scenario Specifications
pry(main)> require './lib/student'
=> true
pry(main)> student = Student.new('Penelope')
=> #<Student:0x007fa71e12c1f0 @cookies=[], @name="Penelope">
pry(main)> student.name
=> "Penelope"
pry(main)> student.cookies
=> []
pry(main)> student.add_cookie('Chocolate Chunk')
pry(main)> student.add_cookie('Snickerdoodle')
pry(main)> student.cookies
=> ["Chocolate Chunk", "Snickerdoodle"]
Now, let’s write tests based on the interaction pattern above.
# student_spec.rb
require 'rspec'
describe Student
# test it exists
# test it has a name
# test it has cookies
# test it can add cookies
end
Let’s build out our Student Test!
# student_spec.rb
require 'rspec'
describe Student do
describe '#initialize' do
it 'is an instance of student' do
student = Student.new('Penelope')
expect(student).to be_a Student
end
end
end
- Write tests
- Run tests
- Thoroughly read errors & failures
- Write implementation code
class Student
end
- Do it all again
# student_spec.rb
require 'rspec'
require './lib/student'
describe Student do
describe '#initialize' do
it 'is an instance of student' do
student = Student.new('Penelope')
expect(student).to be_a Student
end
it 'has a name' do
student = Student.new('Penelope')
expect(student.name).to eq 'Penelope'
end
end
end
class Student
def initialize(name)
@name = name
end
def name
@name
end
end
S.E.A.T
Each test that we create needs 4 components to be a properly built test.
- Setup - The setup of a test is all of the lines of code that need to be executed in order to verify some behavior. Because each test is run individually, we often see the same setup being created multiple times.
- Execution - The execution is the actual running of the method we are testing. This sometimes happens on the same line as the assertion, and sometimes happens prior to the assertion.
- Assertion - The verification of the behavior we are expecting. This is really the main focus of the test; without the assertion, we have no test.
- Teardown - After we complete a test, we want to delete all of our setup, and clear the scope for our next test. In RSpec this is done automatically! So, you won’t need to worry about this in Mod 1, but it is important to know for other testing frameworks.
With a partner, see if you can identify each of the components in the following tests:
# student_spec.rb
require 'rspec'
require './lib/student'
describe Student do
describe '#initialize' do
it 'is an instance of student' do
student = Student.new('Penelope')
expect(student).to be_a Student
end
it 'has a name' do
student = Student.new('Penelope')
expect(student.name).to eq 'Penelope'
end
it 'has cookies by default' do
student = Student.new('Penelope')
expect(student.cookies).to eq []
end
end
describe '#add_cookie' do
it 'adds cookie to cookies array' do
student = Student.new('Penelope')
student.add_cookie('Chocolate Chip')
student.add_cookie('Snickerdoodle')
expect(student.cookies).to eq ['Chocolate Chip', 'Snickerdoodle']
end
end
end
Testing Cont. Warmup
What do you think the following .to
methods do?
.to eq
.to be_a
.to be true
.to be_nil
.to include
Additional Test Intricacies
Ensuring Dynamic Functionality
We should make sure that all of our methods can handle different cases, ensuring that our implementation code is dynamic, e.g.:
# student_spec.rb
require 'rspec'
describe Student do
describe '#initialize' do
it 'is an instance of student' do
student = Student.new('Penelope')
expect(student).to be_a Student
end
it 'has a name' do
student = Student.new('Penelope')
expect(student.name).to eq 'Penelope'
end
it 'has a different name' do
student = Student.new('James')
expect(student.name).to eq 'James'
end
end
end
Testing Edge Cases
- Ensure that your implementation code can handle things we might not expect, e.g.:
# student_spec.rb
describe Student do
describe '#initialize' do
it 'is an instance of student' do
student = Student.new('Penelope')
expect(student).to be_a Student
end
it 'has a name' do
student = Student.new('Penelope')
expect(student.name).to eq 'Penelope'
end
it 'assigns a default name' do
student = Student.new(42)
expect(student.name).to eq 'Default Name Assigned'
end
end
end
Nice to Know
- Each test is independent of the next; don’t depend on tests to run in order of how they’re written
- However, it clarifies your code to other humans to write in order of complexity; aim to start from most basic to most complex functionality and keep tests grouped by method
- You can
before(:each)
method- This will provide shared test setup run before each individual test
- Tests will generally return an
E
for error,F
for failure &.
for passing
describe Student do
before(:each) do
@student = Student.new('Penelope')
end
end
Recap
- What 2 directories should we have within our project directory?
rspec
setup- What do you have to require in a spec file?
- What goes in the initial describe block?
- What is the syntax for a RSpec spec?
- Name 3
.to
methods you learned about today & describe their syntax.
Note
We are using Rspec to test our Ruby code. However, the testing approach outlined above applies to programming in general. There are many libraries and frameworks to choose from in order to test code written in various langages. The syntax may vary but the overall approach is very similar regardless.
For example, here’s what the testing might look like in Javascript, using Mocha testing framework and Chai assertion library.
// student-test.js
var assert = require('chai').assert;
var expect = require('chai').expect;
var Student = require('./Student.js');
describe('Student', function () {
describe('initialize', function () {
it('should be an instance of Student', function () {
// setup for test (if necessary)
// execute function
let newStudent = new Student('Penelope')
// assert what the result SHOULD be
expect(newStudent).to.be.an.instanceOf(Student)
})
it('should have a name', function () {
let newStudent = new Student('Penelope')
expect(newStudent.name).to.equal('Penelope')
})
it('should have cookies by default', function () {
let newStudent = new Student('Penelope')
expect(newStudent.cookies).to.deep.equal([])
})
})
describe('addCookie', function () {
it('should add cookie to cookies array', function () {
let newStudent = new Student('Penelope')
newStudent.addCookie('Chocolate Chip')
newStudent.addCookie('Snickerdoodle')
expect(newStudent.cookies).to.deep.equal(['Chocolate Chip', 'Snickerdoodle'])
})
})
})