- Explore the RSpec testing environment
- Read and interpret tests
- Practice declaring and using variables
We've covered a lot of the basics of Ruby, but, before we go further, we must talk more about testing. As part of this course, you will encounter many lessons with tests that must be passed to register the lesson as complete. These are referred to as labs. You've already completed a few of them! In all labs, you follow a similar process:
- Work in the provided files, testing out potential solutions: in this lab, for
instance, code will need to be written in
calculator.rb
to pass the tests - Run
learn
to print the tests at any point while you're writing your code - Read the error messages produced by running the tests
- Write code that will resolve these error messages
- Run
learn
again to check your progress - Repeat until all tests are passing
- Run
learn submit
to submit your solution
As the lesson material becomes more complex, so do the tests. Getting acquainted with reading and interpreting tests will help you overcome some of the toughest labs ahead. In this lesson, we're going to walk through reading tests in detail while also getting a little more practice with variables.
Your task in this lab is to build a simple calculator. Using variables, this calculator will be able to take any two numbers and produce the result of their addition, subtraction, multiplication, and division. The exact specifications we need to create this calculator are available to us... in the tests.
When we want to run an experiment, we need to develop a hypothesis and we need to test it. In programming, we run tests to verify that programs behave the way we think they do. Tests help us identify bugs and judge how healthy our applications are.
We use tests to describe the program's behavior, just as you would in a professional coding environment. We also use them as teaching tools. You are in charge of getting the tests to pass.
In Ruby, tests are handled by a tool called RSpec. RSpec is written in Ruby, but as we will see, has some custom functionality built in specifically for writing and running tests.
The structure of this lab—where its files and folders are located—looks roughly like the following:
├── CONTRIBUTING.md
├── LICENSE.md
├── README.md
├── calculator.rb
└── spec
└── calculator_spec.rb
└── spec_helper.rb
All labs will more or less have the same structure. (non-lab lessons will still
have CONTRIBUTING.md, LICENSE.md, and README.md files.) In Ruby, all labs will
have a spec
folder that contains our tests.
Open up calculator.rb
in your text editor. If you're using the Learn IDE,
click the blue "Open IDE" button in the top right-hand corner of the lesson. If
you open up that
programming-univbasics-3-labs-with-tdd/
directory, you'll see a list of files (along with a spec/
directory). Click
calculator.rb
, and it will open in the editor.
In calculator.rb
, you should see, well, nothing. We'll fix that soon.
Now open up spec/calculator_spec.rb
. Hey, there's something! What's all of
this stuff doing?
Note: The spec/calculator_spec.rb
has great info that we want to look at,
but do not edit this file. Otherwise, you may have extra difficulty passing
this lab.
A few lines down in the spec/calculator_spec.rb
file you will see:
describe "./calculator.rb" do
# Lots of code written inside here
end
describe
is a method provided by our test library, RSpec. Remember, methods
let us group up a set of statements into a sort of bundle. Whenever that
bundle's name is called, all the statements inside are called in order. This is
incredibly useful when we need to run the same statements over and over (as we
do running and rerunning tests).
The describe
method holds our tests. Just after describe
is a string,
"./calculator.rb"
. Here, RSpec is telling us that the tests that come
afterward will be about the file calculator.rb
. We see the first one inside
another RSpec method, it
:
it "contains a local variable called first_number that is assigned to a number" do
# code for first test is in here
end
The it
method has a longer string describing what this test is for. In this
case, since this is within the describe
method, we can infer that
calculator.rb
contains a local variable called first_number
, and that the
variable is assigned to a number.
Looking at what is inside, we can see the actual test that has been described:
it "contains a local variable called first_number that is assigned to a number" do
first_number = get_variable_from_file('./calculator.rb', "first_number")
expect(first_number).to be_an(Integer).or be_a(Float)
end
Reading the first line, we see a variable, first_number
being assigned to
something, get_variable_from_file
. This is actually another method! For now,
we don't need to know what this method is doing (although, given its name, we
could probably guess). All we need to know is that the result of
get_variable_from_file('./calculator.rb', "first_number")
is getting assigned
to first_number
.
Two lines down, we see something else that is new: expect
.
expect(first_number).to be_an(Integer).or be_a(Float)
This is the actual test that will produce a passing or failing response when
we run learn
. Read out loud, this line sounds like a normal English sentence:
Expect first_number to be an integer or be a float.
expect
is another RSpec method, indicating a test statementfirst_number
is the variable defined two lines prior. It is the subject of the testto
allows the test to define a positive expectation. Usingnot_to
here would define a negative expectationbe_an
/be_a
are known as RSpec matchers. In this case, they are for setting up the expectation that something is a certain data typeor
allows for two possible passing scenarios here: eitherfirst_number
is an integer orfirst_number
is a float.
If we run learn
this test appears first, along with the string descriptions
we saw in describe
and it
:
./calculator.rb
contains a local variable called first_number that is assigned to a number (FAILED - 1)
Failures:
1) ./calculator.rb contains a local variable called first_number that is assigned to a number
Failure/Error: raise NameError, "local variable #{variable} not defined in #{file}."
NameError:
local variable first_number not defined in ./calculator.rb.
# ./spec/spec_helper.rb:14:in `rescue in get_variable_from_file'
# ./spec/spec_helper.rb:11:in `get_variable_from_file'
# ./spec/calculator_spec.rb:6:in `block (2 levels) in <top (required)>'
# ------------------
# --- Caused by: ---
# NameError:
# local variable `first_number' is not defined for #<Binding:0x00007fb7db153ca0>
# ./spec/spec_helper.rb:12:in `local_variable_get'
What if we were to create a variable called first_number
inside
calculator.rb
, but assigned it to something other than a number? Say, for
instance, we wrote first_number = "Hello world!"
in calculator.rb
. Running
learn
again, we would see this:
./calculator.rb
contains a local variable called first_number that is assigned to a number (FAILED - 1)
Failures:
1) ./calculator.rb contains a local variable called first_number that is assigned to a number
Failure/Error: expect(first_number).to be_an(Integer).or be_a(Float)
expected "Hello world!" to be a kind of Integer
...or:
expected "Hello world!" to be a kind of Float
# ./spec/calculator_spec.rb:8:in `block (2 levels) in <top (required)>'
Notice that the error message has changed. The test was able to get the
variable first_number
that we defined inside calculator.rb
. It then runs
the test on first_number
to see if it is a kind of integer or float. It isn't
either, so the test fails.
That's a lot to take in. Don't worry too much yet if it's hard to understand
what is happening inside of the spec/calculator_spec.rb
file. But it's a good
idea to open up the file and gather the information that you can, especially
when you are stuck in a lab. Reading test files and their results can give
targeted insight into what is breaking and where when you're writing a solution.
We will also provide instructions in the README.md
file that will allow you to
complete the lab. However, the better you understand the tools you are using,
the better equipped you will be as we progress to more and more complex topics.
Now that you've got a sense of the tests in this lab, it is time to solve them
all. There are six tests in total. In this lab, testing is configured with
--fail-fast
, so you will only see the first test that fails along with any
passing tests before that. This means, as you pass each test, you'll see a
growing list of passing tests until you've passed them all. Run learn
as you
work through each step below to see the test results.
-
The first test we've started to solve already. The test is looking for a variable in
calculator.rb
,first_number
. This variable should be set to an integer or float -
The second test is similar, but this time, looking for
second_number
. However, there is a second test here that must also pass:
expect(second_number).not_to equal(0)
-
The third test is looking for a local variable named
sum
. Thesum
variable is the result of addingfirst_number
andsecond_number
together. This test is using all three variables. Not only that, the test is using whatever values you assigned tofirst_number
andsecond_number
. -
The fourth, fifth and sixth tests are similar to the tests for
sum
. Create the variabledifference
for subtracting,product
for multiplying, andquotient
for dividing thefirst_number
andsecond_number
variables.
Hint: If you're stuck on a particular variable, try writing the variable in
calculator.rb
and assigning it a value you know is incorrect. Tests may produce different bits of useful information based on what you've written.
Once you have all tests passing, run learn submit
to submit your solution.
Note: Many labs in this course will show all the tests, both passing and failing by default. You can use
--fail-fast
to change this behavior on any lab. Just typelearn --fail-fast
when running the tests.
Labs are a major part of this course and all labs rely on tests that you must pass to register the completion of the lesson. Being able to read and interpret tests will help you unravel complex challenges ahead. More than that, though, testing is a powerful tool you can use in your future development work.
Test-driven development is a common process for developing programs in which tests are written before code. A feature is first designed. Those designs are then set up as test expectations. Once the tests are created, the actual code is written to pass those tests.
Writing our own tests is still a bit further down the path of learning Ruby, but being able to read tests is a skill that will be immediately helpful to you.