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