Skip to content
This repository has been archived by the owner on Oct 1, 2018. It is now read-only.

Test Driven Development Lessons #114

Open
3 of 5 tasks
KevinMulhern opened this issue Jan 13, 2018 · 7 comments
Open
3 of 5 tasks

Test Driven Development Lessons #114

KevinMulhern opened this issue Jan 13, 2018 · 7 comments
Assignees

Comments

@KevinMulhern
Copy link
Member

KevinMulhern commented Jan 13, 2018

We are seeking people to take charge of coordinating the creation of these lessons. If you would like to fill take this role, please let us know by commenting on this issue with your desire take ownership of these lessons.

For each lesson, reply to this issue with:

  1. An Introduction for the lesson
    A brief summary about what the lesson is about and why the topics or concepts it teaches are important.

  2. Learning Outcomes
    A list of bullet points of what the student is expected to know or be able to do by the end of this lesson

  3. Content for the lesson
    Explanations of the main concepts in the lesson, include code snippets and easy to understand metaphors where applicable.

  4. Assignment
    Either a list of links to resources the user will go through to learn about the topic of this lesson more in depth. Have no more than 5 resources, ideally no more than three. Or an exercise the student should do to solidify their understanding of the lesson content.

  5. Additional Resources
    A list of links to other resources which are valuable and or complement the assignment resources. Link to no more than three additional resources to avoid this section becoming too cluttered.


List of lessons:

  • Why TDD? -The philosophy behind TDD and its process, red, green refactor.
  • Introduction to RSpec - Install Rspec and its basic syntax
  • Matchers - The basic Matchers you will use in RSpec and when to use them
  • Mocks, stubs and spies - What each of these are and when and how to use them
  • What to test - The rules of what to test in unit tests and how to structure them
@KevinMulhern
Copy link
Member Author

KevinMulhern commented Jan 13, 2018

Introduction to RSpec lesson done by @leosoaivan

Introduction

In the previous lesson, we established the utility of test-driven development (TDD) in maintaining your code and sanity. In this lesson, we'll introduce you to your new best friend, the RSpec testing framework. RSpec is one of the most popular testing frameworks, having been downloaded over 100 million times and having been ported for use in Rails development.

Learning Outcomes

Look through these now and use them to guide your learning. By the end of this lesson, expect to:

  • Know what RSpec is
  • Know how to install RSpec
  • Understand basic RSpec syntax:
    • describe
    • it

Introduction to RSpec

What is RSpec, and why RSpec?

At the most basic level, RSpec is a Domain Specific Language written in Ruby, or, for the rest of us, a language specialized for a particular task. In this case, the task is testing Ruby code. The rspec gem comes packaged with all you need to get started, including five gems: rspec-core, rspec-expectations, rspec-mocks, rspec-rails, and rspec-support.

At this point, you may be wondering, Why RSpec? Surely, there are other frameworks out there, and you'd be right. There are. In fact, at one point, Ruby came bundled with Test::Unit and later Minitest as part of its standard library, the latter of which lives on in Rails. If you tend to be pessimistic (I'm sorry, I meant realistic), then the Wrong testing framework might be your cup of tea. Or perhaps you're hungry and in the mood for something more substantial, in which case a side of Bacon might be what you need. At the end of the day, it doesn't matter which framework you choose as long as you cultivate your testing skills.

The reason we choose RSpec is for its transparency. Just like Ruby itself, RSpec was written to be pleasant to read and write. It's easy to write Rspec tests that are clear and understandable. This might seem like a simple reason to choose a framework, but the simplicity of this choice is a proxy for deeper and more fundamental goals to programming. First, transparency means that testing is easier to adopt as a beginner. The sooner you adopt testing, the sooner you can be sure that your application works as intended. Second, transparency means that the intentions and targets of your tests are clear to you and to your collaborators. Not only is this helpful in developing and testing for edge cases, but it can also make it much easier to maintain a test suite as your application grows. An evolving and well-maintained test suite provides your application with a safety net as it too evolves and grows. A well-maintained test suite ensures that your code can meet regressions with aplomb and can remain adaptable to inevitable change.

TLDR: RSpec is easy to learn, easy to write, and easy to maintain.

But enough proselytizing. Strap your helmet and buckle up; we're going to jump right in.

Installing RSpec

Boot up your terminal and punch in gem install rspec to install RSpec. Once that's done, you can verify your version of RSpec with rspec --version, which will output the current version of each of the packaged gems. Take a minute also to hit rspec --help and look through the various options available.

Finally, cd into a project directory that you wish to configure for use with RSpec and type rspec --init to initialize RSpec within the project. This will generate two files, .rspec and spec/spec_helper.rb, such that your project might look like:

project
  |__lib
  |   |__script.rb
  |
  |__spec
  |   |__spec_helper.rb
  |
  |__.spec

That's it. Within two steps, you're up and running with RSpec. That was so hard, wasn't it?

Basic syntax

How 'bout a test to see the syntax? Create a new directory called "ruby_testing", change into it, and initiate RSpec.

$ mkdir ruby_testing
$ cd ruby_testing
$ rspec --init

As expected, the output will read:

  create   .rspec
  create   spec/spec_helper.rb

Run the tests from your terminal by using the rspec command, which will return "No examples found." That really shouldn't surprise you, because we haven't written any tests yet. If you're still shocked...maybe take a short break, or come say hello to us in our Ruby Gitter room.

No examples found.


Finished in 0.00037 seconds (files took 0.21108 seconds to load)
0 examples, 0 failures

Let's add our first test. Let's say we want to create a calculator with a few methods that we want to test. True to TDD, we will write the tests prior to the code. The spec/ folder is where all your tests will live. Using touch on the command line or through your text editor, create calculator_spec.rb within the spec/ folder and add the following lines:

#spec/calculator_spec.rb

RSpec.describe Calculator do
  describe "#add" do
    it "returns the sum of two numbers" do
      calculator = Calculator.new
      expect(calculator.add(5, 2)).to eql(7)
    end
  end
end

Let's go line by line.

First, describe is an RSpec keyword that defines an "Example Group", or a collection of tests. It takes a class or a string as an argument and is passed a block. describe blocks can be nested, such as on the second line of our test above. When describing a class, the following syntax is also valid:

#spec/calculator_spec.rb

describe Calculator do
  #...
end

The it keyword defines an individual example (aka, test). it takes a string argument and is also passed a block. This block is where our expectations of a method are expressed. In this particular case, when we pass 5 and 2 to the #add method, we expect it to return 7. This is concisely expressed in our expectation clause, which uses one of RSpec's equality matchers, eql:

expect(calculator.add(5, 2)).to eql(7)

Simple, isn't it? One more time, from the top:

  1. describe the class
  2. describe the method example group. Conventionally, the string argument for instance methods are written as "#method", while string arguments for class methods are written as ".method".
  3. Write your test case/example with it.
  4. Write your expectation using expect. The expect method is also chained with .to for positive expectations, or .to_not/.not_to for negative expectations. We prefer .not_to. Also, limit one expect clause per test case.

Passing code

Let's move on. Run rspec from the directory root, and watch the output.

An error occurred while loading ./spec/calculator_spec.rb.
Failure/Error:
  describe Calculator do
    describe "#add" do
      it "returns the sum of two numbers" do
        calculator = Calculator.new
        expect(calculator.add(5, 2)).to eql(7)
      end
    end
  end

NameError:
  uninitialized constant Calculator
# ./spec/calculator_spec.rb:1:in `<top (required)>'
No examples found.


Finished in 0.0004 seconds (files took 0.16461 seconds to load)
0 examples, 0 failures, 1 error occurred outside of examples

So our first test returned an error. This is unsurprising. NameError is essentially telling us that RSpec looked for a Calculator class, but couldn't find one. So let's create it. From your project root, create a lib/ folder, and inside, calculator.rb with your class. We'll also go ahead and begin the #add method, otherwise RSpec will give us a similar error as the previous one when it looks for it:

#lib/calculator.rb

class Calculator
  def add(a,b)
  end
end

Finally, we must also tell the spec where the Calculator class is being defined. This is easily done with require:

#spec/calculator_spec.rb
require './lib/calculator'

describe Calculator do
  #...
end

If you were to run rspec this time, you'd get your first failure!

F

Failures:

  1) Calculator#add returns the sum of two numbers
     Failure/Error: expect(calculator.add(5, 2)).to eql(7)
     
       expected: 7
            got: nil
     
       (compared using eql?)
     # ./spec/calculator_spec.rb:7:in `block (3 levels) in <top (required)>'

Finished in 0.28565 seconds (files took 0.6273 seconds to load)
1 example, 1 failure

Failed examples:

rspec ./spec/calculator_spec.rb:5 # Calculator#add adds two numbers together

Our first failure is denoted by the F at the top of the output. Congratulations! You've made it to the "red" portion of the "red-green-factor" cycle of TDD. RSpec also provides a list of all the failures, with the expected vs. actual output of the method being tested. At the bottom of your output, RSpec also points to the line of the failing test, which in this case is where our it block started.

Getting this method to "green" shouldn't be too difficult. RSpec clearly provides a reason for the failure: it expected the output to be 7 when we provided the method with (5, 2) as the parameters. Instead, it returned nil. Why might that be? Well, our #add does take two parameters...but it does nothing with them! Add the minimum amount of code necessary to get your test to pass:

#lib/calculator.rb

class Calculator
  def add(a,b)
    a + b         #=> add this
  end
end

Then, run the test again to get a single dot, letting you know that your test has passed:

.

Finished in 0.0032 seconds (files took 0.14864 seconds to load)
1 example, 0 failures

At this point, refactoring isn't necessary. The #add method is essentially a one line method. As you progress in your Ruby learning, however, you might find your methods getting more complex, and you might find that you have to make extra efforts to abide by SOLID principles. When that time comes, using RSpec and the "red-green-refactor" cycle will allow you to code with confidence, knowing that your classes and their behaviors continue to meet your specified expectations.

Assignments

It's time to put your newfound knowledge to good use. Let's break our Calculator test.

  1. Let's implement a new test case for your #add method, written out for you below. Run the test to see the failure. Write the minimum code necessary to get both tests to pass, then refactor if necessary.
#spec/calculator_spec.rb

describe Calculator do
  describe "#add" do
    it "returns the sum of two numbers" do
      #removed for brevity
    end

    #add this
    it "returns the sum of more than two numbers" do
      calculator = Calculator.new
      expect(calculator.add(2, 5, 7)).to eql(14)
    end
  end
end
  1. Write a test for a new Calculator method (#multiply, #subtract, or #divide) using a new describe block. Include at least one it block with an appropriate expectation clause. Get it to pass, and refactor if necessary.
  2. In the terminal, try running your failing or passing tests with rspec --format documentation. What's different?
  3. RSpec reads command line configurations from .rspec, one of the two files generated when RSpec is initialized in a project. If you liked the output you got with --format documentation, you can use the .rspec file to hold that flag. In doing so, you won't have to type it in every time you run your test suite. Open the file in your text editor and, on a new line, add --format documentation. For more information on configuring RSpec, see the docs here.

Additional Resources

This section contains helpful links to other content. It isn't required, so consider it supplemental for if you need to dive deeper into something

  • Briefly look over RSpec's other matchers, if you haven't done so already.
  • Briefly look over the RSpec styling and syntax recommended by BetterSpecs and read through the first six guidelines.
  • The RSpec Cheat Sheet should help you avoid Googling every new bit of syntax.

@KevinMulhern
Copy link
Member Author

We're going to reopen this issue when this course is merged back into the curriculum repo (commit history will be preserved)

@leosoaivan leosoaivan self-assigned this Jun 8, 2018
@KevinMulhern KevinMulhern reopened this Jun 8, 2018
@leosoaivan
Copy link
Contributor

🎉

@Deetss
Copy link
Contributor

Deetss commented Jun 19, 2018

Hope yall approve of my matchers lesson.

Introduction

In the previous lesson, we went over the basics of writing tests with RSpec. In this lesson, we will introduce you to RSpec matchers. There are tons of RSpec matchers and we couldn't possibly cover all of them in this lesson but I encourage you to take a look at the entire list of RSpec matchers

Learning Outcomes:

  • Understand what RSpec matchers are
  • Be able to craft more complex expectations
  • Know the most commonly used matchers

RSpec Matchers

Why do you need to know about RSpec matchers?

You may be asking yourself "Why do I need to know about matchers?" Well, matchers are a very important part of an expectation. Without them, you wouldn't be able to test anything!

What are RSpec matchers?

To understand matchers well, we need to take a step back. Rspec gives you two methods for your expectations to and not_to. Each of those methods takes one parameter, the matcher. So in essence, the matcher will follow right after expect(actual).to. Here are some examples of basic matchers in use.

describe "An example of basic matchers" do
  it "Shows some matchers" do
                          #{matcher}
                          #    |
                          #    V
    expect(something).to be_true
    expect(something).to eq(3)
    expect(something).to_not raise_error(SomeError)
  end
end

RSpec also gives you access to some special matchers when you have a predicate (boolean) method. Let's say you have a calculator class, and that class has two methods even? and odd?. RSpec will create two matchers special for that class be_even and be_odd.

Let's take a look at those matchers in action.

describe "An example of predicate matchers" do
  it "should add two numbers and be even" do
                                  #{matcher}
                                  #    |
                                  #    V
    expect(@calculator.add(2,2)).to be_even
    expect(@calculator.add(2,2)).to_not be_odd
  end
end

As you can see there's not much to the basic matchers but they are essential to testing and making sure you're comfortable with matchers is a good step towards testing your applications well.

Basic RSpec matchers

I have just gone over some of the very basic matchers built into RSpec. There are a ton more, but these are the most common ones you may encounter in the wild. Run through this list of matchers and make sure you understand which part of the expectation is a matcher

describe "The most common RSpec matcher" do
  it "Shows some more matchers" do
    expect(array).to all(matcher)
    expect(actual).to be > expected # (also works with =, <=, and ==)
    expect(actual).to be_a(type)
    expect(actual).to be_truthy
    expect(actual).to be_falsy
    expect(actual).to be_nil
    expect(actual).to be_between(min, max)
    expect(actual).to be_within(delta).of(expected)
    expect { block }.to change(receiver, message, &block)
    expect(actual).to contain_exactly(expected)
    expect(range).to cover(actual_value)
    expect(actual).to eq(expected)
    expect(actual).to exist expect(actual).to have_attributes(key/value pairs)
    expect(actual).to include(*expected)
    expect(actual).to match(regex)
    expect { block }.to output(value).to_stdout # also to_stderr
    expect { block }.to raise_error(exception)
    expect(actual).to satisfy { block }
  end
end

Assignments

  1. If you haven't already taken a look at the RSpec Matchers Docs go take a look. You don't have to read everything now but have a look at what is offered.
  2. Next go here and read through this page, it gives you a nice overview of some of the other RSpec matchers and how you can use them. https://www.tutorialspoint.com/rspec/rspec_matchers.html
  3. RSpec allows you to create custom matchers. Read this blog post on creating your own custom matchers. https://danielchangnyc.github.io/blog/2014/01/15/tdd2-RSpecMatchers/

Additional Resources

@KevinMulhern
Copy link
Member Author

Hey @Deetss, the lesson looks great 😍

It would probably be worth wrapping any expectations in examples within it blocks. Just as a precaution incase students think they can make expectations outside of it blocks (If an expectation isn't in an it block it will raise an error normally)

Thats the only thing I notice, everything else looks good. Nice work man!

@Deetss
Copy link
Contributor

Deetss commented Jun 20, 2018

Okay awesome! Thanks for y'all feedback!

@leosoaivan
Copy link
Contributor

Test-Driven Development

Introduction

Hopefully, you've been having fun developing in Ruby thus far. Perhaps one thing you may not have enjoyed as much, though, is having to run your script on the command line, running through each step manually, only to find that it doesn't return what you expected. And then comes the even-less-fun part: You tug at your hair a bit, change a few variables, and run your script again through each step, hoping that the results are correct.

Folks, we're here to tell you there IS better. Hoping is for that exam you didn't study for and took by the seat of your pants. Knowing is test-driven development.

Learning Outcomes

  • Understand what test-driven development (TDD) is, and why it's important.

What is Test-Driven Development?

Test-Driven Development (TDD) is a process and technique of software development that relies on the repetition of a very short and specific development cycle. In each cycle, requirements (i.e., what you want your code to do) are turned into specific test cases first. These requirements could be anything from an entire feature that requires end-to-end (E2E) testing, such as a tests that cover a user logging into your website succesfully AND unsuccessfully, to a new Ruby class you've devised, for which unit tests might suffice. Either way, the test suite for these requirements fail initially, since actual code hasn't been written yet.

Once the code is written and passes our test suite, you can move onto refactoring. As you might have guessed, running the test suite against the changes we make to our features or units enables us to refactor while making sure everything still works.

Colloquially, this process is often referred to as the "red-green-refactor" cycle. That's all it is, in a nutshell: automated tests drive the design of software. You don't need to know how the entire architecture of your sweet, new, industry-breaking application will work. Your application only has to be broken down, step-by-step, until small units are identified and covered by tests.

Why Is It Important?

Even if TDD intuitively seems like good practice, its utility is hotly debated even today.

No one is saying that tests aren't useful. The question is whether or not tests should drive development, or whether or not it's important for them to come first. Many developers write tests after they've written their code. There is definitely a longer list of pros than cons for having tests in general—no one will disagree with that—so why should we test first?

Here are some reasons we think it might be important for you to implement TDD:

  1. Practice. Many employers look for developers who have experience or interest in testing. Sure, you can write tests later, but will you really? Test first for good measure.
  2. Effectiveness. Writing tests before code is like stretching before a workout. It's not required, but your workout won't be as good. Writing tests first ensures that the code that comes after works and is of high quality.
  3. Efficiency. You'll save time in the long run by not having to re-run entire applications just to make sure little parts work.

Assignments

  1. Read up on 9 Benefits of Test-Driven Development for additional insight on this practice. Note that these points may be more applicable to testing in general rather than TDD specifically, but they are still good points nonetheless.
  2. For a more rounded take, consider DHH's "TDD is Dead. Long Live TDD", which sparked broad controversial, conversations on what TDD is, what it has become, and what it should be.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants