Intro to TDD

Intro to Test-Driven Development (TDD)

Learning Objectives

  • Explain and demonstrate the TDD workflow
  • Create and run a minitest test suite (including assertions)
  • Interpret error messages & stack trace; adjust implementation code to fix error messages
  • Interpret test failures & fix implementation accordingly

Vocabulary

  • TDD
  • Test Error/Failure
  • Stack Trace
  • minitest assertion

Warm-up

  • What are some things we need for the setup of tests?
  • Name a few assertion methods.
  • Where should we run our tests from?
  • What do error messages tell us?

Test-Driven Development (TDD) Overview

As we write increasingly complex applications, we’ll need more sophisticated testing approaches to secure the same level of confidence.

Advantages

TDD is a process for writing code that helps:

  • Drive your development! (Intentional coding)
  • Ensure your code works the way you intend
  • Create code that is understandable to others
  • Create code that is cheap and easy to change

Difficulties

  • Learning a Domain Specific Language (DSL; a computer language specialized to a particular application domain)
  • Planning what we want to happen
  • Taking bite sized chunks

Using TDD

  • Write tests before you write code (radical, we know)
  • Use a testing framework such as minitest to structure your testing suite
  • Red-Green-Refactor process to implement complexity to your application

TDD Cycle: Red, Green, Refactor

Red-green-refactor is a process for writing code that involves three steps.

  1. Write a failing test (red)
  2. Write implementation code to make the test pass (green)
  3. Clean up your code if necessary (refactor)

Write Tests First

  • Shapes design
  • Helps break problem into small pieces
  • Removes fear of programming
  • Communicates what your code should do
  • Tells you basically exactly what to do

TDD Code-Along with minitest

Scenario Specifications

  • Products have a name
  • Products have a description
  • Products have a price
  • Products start with a total stock number available
  • You can calculate the cost of the total inventory
# product_test.rb
require 'minitest'
require 'minitest/autorun'
require 'minitest/pride'

class ProductTest < Minitest::Test
  def test_it_exists
    doomproof_vest = Product.new('Doomproof platinum vest', 'A radiation-absorbing tunic that allows the wearer to survive exposure to doom radiation.', 1200.00, 20)
    
    assert_instance_of Product, doomproof_vest
  end
  # test it has a name
  # test it has a description
  # test it has a price
  # test it has an initial stock number
  # test you can calculate the cost of the total inventory
end

Learn to Love the Error, Learn to Love the Failure

They’re your friends, seriously. Take time to understand each error and failure you encounter. You’ll be seeing those same error messages over and over again, so the sooner you connect what they mean to what you need to fix, the smoother you’ll be sailing.

  • Errors usually indicate your code is broken somewhere
  • Failures usually indicate that your code isn’t functioning the way you expect it to

Solving an Error or a Failure

  1. Run the test
  2. Read the error/failure message, including stack trace
    • stacks are LIFO (last-in, first-out like a stack of dishes)
    • call stack is a stack that stores information about the active subroutines of a computer program (often referred to as ‘the stack’)
    • stack trace is a report of the active stack frames at a certain point in time during the execution of a program
    • fun-fact: queues are FIFO (first-in, first-out, like a European line-up)
  3. Write implementation code to make the test pass
  • Lets write some implementation code to solve our errors, one step at a time.
E

Error:
ProductTest#test_it_exists:
NameError: uninitialized constant ProductTest::Product
Did you mean?  ProductTest
    product_test.rb:8:in `test_it_exists'
# product_test.rb
require 'minitest'
require 'minitest/autorun'
require 'minitest/pride'
require './lib/product'

class ProductTest < Minitest::Test
  def test_it_exists
    doomproof_vest = Product.new('Doomproof platinum vest', 'A radiation-absorbing tunic that allows the wearer to survive exposure to doom radiation.', 1200.00, 20)
    
    assert_instance_of Product, doomproof_vest
  end
  # test it has a name
  # test it has a description
  # test it has a price
  # test it has an initial stock number
  # test you can calculate the cost of the total inventory
end

class Product
  
end
Error:
ProductTest#test_it_exists:
ArgumentError: wrong number of arguments (given 4, expected 0)
    product_test.rb:9:in `initialize'
    product_test.rb:9:in `new'
    product_test.rb:9:in `test_it_exists'
class Product
  
  def initialize(l, l, l, l)
    #code
  end
  
end
  • Refactor
class Product
  
  def initialize(name, description, unit_price, amount_available)
    #code
  end
  
end
.
def test_it_has_a_name_product_price_and_amount_available
  doomproof_vest = Product.new('Doomproof platinum vest', 'A radiation-absorbing tunic that allows the wearer to survive exposure to doom radiation.', 1200.00, 20)
  
  assert_equal 'Doomproof platinum vest', doomproof_vest.name
  assert_equal 'A radiation-absorbing tunic that allows the wearer to survive exposure to doom radiation.', doomproof_vest.description
  assert_equal 1200.00, doomproof_vest.unit_price
  assert_equal 20, doomproof_vest.amount_available
end
.E

Error:
ProductTest#test_it_has_a_name_product_price_and_amount_available:
NoMethodError: undefined method `name' for #<Product:0x007fd15901b698>
    product_test.rb:17:in `test_it_has_a_name_product_price_and_amount_available'
class Product
  
  attr_reader :name
  
  def initialize(name, description, unit_price, amount_available)
    @name = name
  end
  
end
  • Continue the TDD cycle to make the current test pass
class Product
  
  attr_reader :name, :description, :unit_price, :amount_available
  
  def initialize(name, description, unit_price, amount_available)
    @name = name
    @description = description
    @unit_price = unit_price
    @amount_available = amount_available
  end
  
end
  • Then dream up how you want your total inventory cost calculation to behave
def test_total_inventory_cost_calculates_total_cost
  doomproof_vest = Product.new('Doomproof platinum vest', 'A radiation-absorbing tunic that allows the wearer to survive exposure to doom radiation.', 1200.00, 20)
  
  assert_equal 24_000.00, doomproof_vest.total_inventory_cost
end
..E

Error:
ProductTest#test_total_inventory_cost_calculates_total_cost:
NoMethodError: undefined method `total_inventory_cost' for #<Product:0x007fc93f143308>
    product_test.rb:26:in `test_total_inventory_cost_calculates_total_cost'
class Product
  ...
  
  def total_inventory_cost
    #code
  end

end
..F

Failure:
ProductTest#test_total_inventory_cost_calculates_total_cost [product_test.rb:26]:
Expected: 24000.0
  Actual: nil
  • Draw the %$&@ing owl
class Product
  ...
  
  def total_inventory_cost
    unit_price * amount_available
  end

end

Turn & Talk

  • What made sense? What was easy?
  • What do you need more practice with?

TDD Practice

  • Repeat TDD process for remaining Student specifications OR
  • Practice TDD with this UNICORN tutorial

Recap

  • What is the color-related catchphrase for TDD workflow?
  • What are some reasons for writing tests before implementation code?
  • What’s the main difference between an error and a failure?
  • What are 3 things an error message tells us?
  • How do you read a stack trace?

Resources

Lesson Search Results

Showing top 10 results