Unit Testing in Ruby 26-Jul-16

advertisement
Unit Testing in Ruby
26-Jul-16
Programming methodologies

The grim facts:



The majority of large programming projects fail
Projects that succeed are usually full of bugs
Modifying and debugging programs usually introduces yet more bugs



Hence, it is hazardous to modify a “working” program
The time spent maintaining a program far exceeds (10x?) the amount of
time it took to write the program
Programming methodologies are attempts to solve these problems
by properly organizing programs and/or programmers




The current (and best) methodologies are the “agile” methodologies
“Agile” means writing software that is easily changed
XP (Exteme Programming) is the best known agile methodology
XP tends to work best for small groups of programmers
Some ideas from agile programming


There is no “silver bullet,” but agile methodologies are
the best we have at present
Most large programming projects fail, so...




“Write the simplest thing that can possibly work.”
Always have a working version, no matter how little it does
Never add a feature until all known bugs have been fixed
Any code that hasn’t been tested is assumed to be wrong



Have tests for all code, and keep it up to date
Run tests frequently—very frequently
Tests must be trivially easy to run, or you won’t bother running them
Test suites

Obviously you have to test your code to get it working in the
first place



You can do ad hoc testing (running whatever tests occur to you at the
moment), or
You can build a test suite (a thorough set of tests that can be run at any
time)
Disadvantages of a test suite

It’s a lot of extra programming


You don’t have time to do all that extra work


This is true, but use of a good test framework can help quite a bit
False—Experiments repeatedly show that test suites reduce debugging time
more than the amount spent building the test suite
Advantages of a test suite


Reduces total number of bugs in delivered code
Makes code much more maintainable and refactorable

This is a huge win for programs that get actual use!
XP approach to testing

In the Extreme Programming approach,



Tests are written before the code itself
If code has no automated test case, it is assumed not to work
A test framework is used so that automated testing can be done after
every small change to the code



This may be as often as every 5 or 10 minutes
If a bug is found after development, a test is created to keep the bug from
coming back
Consequences



Fewer bugs
More maintainable code
Continuous integration—During development, the program always
works—it may not do everything required, but what it does, it does right
Terminology

A test fixture sets up the data (both objects and primitives) that
are needed to run tests

Example: If you are testing code that updates an employee record, you
need an employee record to test it on

A unit test is a test of a single class
A test case tests the response of a single method to a particular
set of inputs
A test suite is a collection of test cases
A test runner is software that runs tests and reports results

An integration test is a test of how well classes work together




JUnit provides some limited support for integration tests
Once more, in pictures
test suite
test runner
another unit test
test case (for one method)

another test case

another unit test
another test case

another test case
another test case


unit test (for one class)
test case (for one method)

another test case

test fixture
A unit test tests the methods in a
single class
A test case tests (insofar as
possible) a single method
You can have multiple test cases
for a single method
A test suite combines unit tests
The test fixture provides software
support for all this
The test runner runs unit tests or
an entire test suite
Integration testing (testing that it
all works together) is not well
supported by JUnit
The test runner

The test runner runs all your tests


If they all succeed, you get a green bar
If any fail, you get a red bar
and links to the tests that failed
How not to write a unit test


Unit tests must be fast and easy to run—or you won’t run them
The only output you should need to look at, in order to see that all
tests passed, at is the green bar

Of course, if you get a red bar, you need to explore further

Don’t do any output from your unit tests!

Ideally, the methods you are testing should not do any output


In most well-written programs, there is a separation of concerns—methods
either compute or do output, but not both
It is possible to write unit tests for methods that do output, but that is a
slightly advanced topic I won’t cover here
How to write a unit test class

A unit test class is a class you write that extends
Test::Unit::TestCase


You will need the line require 'test/unit'
Your test class will inherit the following methods:

def setup



def teardown()



This a method that will be called before each of your test methods
Typically, you will override this method and use it to assign values to
some instance variables you need in testing
This a method that will be called after each of your test methods
Typically you will just ignore this method, unless you need to close files
You will also write any number of test methods, all of which
have the form def test_Something


Something is usually, but not necessarily, the name of the method you
want to test
Inside each test method, you will do some computations and call one or
more assert methods to test the results
Available assertion methods


assert boolean
assert_equal expected, actual


assert_same expected, actual









Uses ==
Uses equal?
assert_not_equal expected, actual
assert_not_same expected, actual
assert nil object
assert not_nil object
assert_block block
All these methods can take an additional message argument
This is not a complete listing of the assert methods
The first two methods are by far the most commonly used
Structure of a unit test

require "test/unit"
require "file_to_be_tested"
class CountTest < Test::Unit::TestCase
def setup
# Perform initializations here
end
def test_some_method
# Tests go here
end
def teardown
# Release any resources (usually not needed)
end
end
Testing for exceptions


Methods should throw exceptions if they are called incorrectly
You can test whether a method throws an exception when it ought
to
def test_exceptions
begin
# Call the method that should throw an exception
rescue Exception # or you can test for specific exceptions
return # The exception happened, so the test passes
end
flunk
end

Ruby also has assert_raise and assert_throws methods, but I
haven’t been able to make them work
A complete example
class Counter
attr_reader :value
def initialize
@value = 0
end
def increment *n
if n == [ ]
@value += 1
else
@value += n[0]
end
end
def reset
@value = 0
end
end
The test class, part 1
require "test/unit"
require "counter"
class CountTest < Test::Unit::TestCase
def setup
@c = Counter.new
end
def test_increment_with_no_args
assert_equal 0, @c.value
@c.increment
assert_equal 1, @c.value
@c.increment
assert_equal 2, @c.value
end
def test_increment_with_arg
assert_equal 0, @c.value
@c.increment 3
assert_equal 3, @c.value
@c.increment 5
assert_equal 8, @c.value
end
def test_reset
@c.increment
@c.increment 10
assert @c.value > 0
@c.reset
assert @c.value.zero?
end
The test class, part 2
def test_exceptions
begin
@c.increment 'four'
return
rescue Exception
end
flunk
end
end # of the test class
Test suites



A test suite is a collection of unit tests
In Ruby, all you have to do is require each test file
Example suite (containing just the one unit test:


require 'ruby_tests‘
Note: In RadRails, running a test suite produces only text output
Test-Driven Development (TDD)

It is difficult to add unit tests to an existing program



The program probably wasn’t written with testing in mind
It’s actually better to write the tests before writing the code you
want to test
This seems backward, but it really does work better:




When tests are written first, you have a clearer idea what to do when you
write the methods
Because the tests are written first, the methods are necessarily written to
be testable
Writing tests first encourages you to write simpler, single-purpose
methods
Because the methods will be called from more than one environment (the
“real” one, plus your test class), they tend to be more independent of the
environment
Recommended approach
1.
Write a test for some method you intend to write
•
2.
3.
4.
Write a stub for the method
Run the test and make sure it fails
Replace the stub with code
•
5.
If the method is fairly complex, test only the simplest case
Write just enough code to pass the tests
Run the test
•
If it fails, debug the method (or maybe debug the test); repeat until the
test passes
6. If the method needs to do more, or handle more complex
situations, add the tests for these first, and go back to step 3
The End
If you don't unit test then you aren't a software engineer, you are a
typist who understands a programming language.
--Moses Jones
1. Never underestimate the power of one little test.
2. There is no such thing as a dumb test.
3. Your tests can often find problems where you're not expecting them.
4. Test that everything you say happens actually does happen.
5. If it's worth documenting, it's worth testing.
--Andy Lester
Download