460 likes | 699 Views
Test-Driven Development (TDD). What is TDD?. changing code without modifying external functional behavior. What is programming?. writing code: addressing requirements/solve a problem verify : code answers requirements / program performs according to specifications
E N D
What is TDD? changing code without modifying external functional behavior
What is programming? • writing code: addressing requirements/solve a problem • verify: code answers requirements / program performs according to specifications • requirement/specification/verification: • complex problem is broken in small problems • can be solved by writing short pieces of code • specify (define) and verify small pieces of code
What is Development? • as we develop more, we understand better… • developer asks better questions, and gets better answers • requirements of program change over time • existing code must be changed... • difficult to predict how other parts of the code are affected
What is Testing? • correctness – code performs by specification • 3 types of tests: • Unit tests: a particular module is working properly: • Implemented by the programmer. • Simple test cases for all functions and methods. A test case is a program… • Integration tests: a combination of modules work well together and exchange messages according to protocols • Acceptance tests: a whole system under test to check that the expected functionality meets the requirements • functional and performance criteria.
Unit Test Terminology • Test case: tests a single scenario of usage. • one aspect of the protocol of the object: its invariant, a single pre-condition / post-condition. • Test suite: collection of test cases that fully verify the public protocol published by an object • Object under test (OUT): the object being tested by a test suite • test suite should focus on testing a single object - assuming all other objects perform correctly
Unit Test Terminology • Test coverage: the part of the code of the object that is executed by running a test suite • every code line of OUT should be executed when running test suite • Test fixture: other objects that will interact with the OUT • A test case must be self-contained: create, initialize and set all the required test fixtures before the test scenario can be executed. Test fixtures must be cleaned up after the test case is run. • Mockup: a basic, test-specific, implementation for classes on which OUT depends. • OUT is independent of other objects - avoid using existing objects • mockup and the "real" object will usually satisfy the same interface • Usually: mockup for data-retrieval, not for logical behavior
Unit Test Terminology • Positive test case: verifies that a public operation of the OUT performs as expected. • Negative test case: verifies that a public operation of the OUT fails as expected • a call to a method when a pre-condition does not hold properly throws an exception • Repeatable and deterministic tests: runs the same test twice in a row - same result. • test case cannot depend on external data or on the timing of RTE scheduler
Pseudocode example set_hour (a_hour: INTEGER) -- Set `hour' to `a_hour' require valid_argument: 0 <= a_houranda_hour <= 23 do hour := a_hour ensure hour_set: hour = a_hour end precondition postcondition
What is TDD? • Test first design - repeatedly first writing a test case and then implementing the code necessary to pass the test. • It is the responsibility of the programmer to define the unit tests as part of the code delivery • requirements granularity level of the module • module depends on other modules
What is TDD? • requirements on the code by writing a test • test case IS the expression of the requirement • implement code to stand by the requirements • code pass tests
Test First Design cycle • Define objects: responsible for specific functionality and interaction • Write tests for each OUT: • Define the interface of the OUT • Define contract for each method of the OUT interface • Specify pre-conditions, post-conditions for each method and invariant for the OUT. • Write test cases for each invariant, pre and post-condition of each method in the interface. • Write the code so it will pass the test • Run tests • Refactor! improve code designby removing code that "looks bad“ ("code smells“) • Repeat process: code changes, rules change, test change • When needed - Break the contract, Redefine tests, Refactor code
Design By Contract (DBC) Concept that helps programmers express the requirements on an object, correctness criteria: • preconditions : things that must be true before we invoke a method • postconditions : things that must be true after a method is invoked • invariants: things that must be true both before and after a method is invoked
TDD benefits • validation of correctness: errors caused by code/design modifications are caught quickly by programmer, immediately after the code is changed. • courage to make code modifications and refactoring, a change will "break" the program. • integration tests - test cases can help design intelligent integration tests. • design improvement- writing tests before the OUT is implemented ensures OUT is indeed usable, that it is easy to prepare the context in which the OUT can be invoked. • E.g.: OUT depends on too many other objects or global objects, - writing tests becomes very difficult - test cases reveal this - strong incentive to improve the design of the OUT • documentation - test classes provide examples of code usage
JUnit Junit: a simple framework to write tests (in Eclipse) • public default ctor • setUp() - prepare pre-conditions (@Before) • tearDown() - "clean up" the test object after a test has run (@After), "undoes" what the @Before method did. • test method for each test case (@Test) • JUnit run: • instance of the test class is constructed. • run setUp() • run test case • display test results (pass/fail) • run tearDown()
TDD • Requirement: write a simple Stack data object, what is a "Stack"? • informal description: • Stack is a container of Objects. • Objects are ordered in Last-In-First-Out order. • Add/Remove an Object to Stack
Interface Turn Description into interface (formalize ) Javadoc: inline tag {@link URL}
Generics • it is a "code smell", to let a container receive any object (too general)… • Object = haven't thought enough about specific objects
fill interface – what means to use a Stack? • DBC: methods, preconditions, postconditions, invariants
test class • test-class skeleton for the interface
define tests • think of complicated scenarios/ usage: parameters, exceptions, return values • think of complicated behavior/sequences: • push-pop-pop, pop-Exception • isEmpty returns true or false, • push objects of different types <T> • push a null - What should we do? • Change Stack API, - add Exception to push() • do not change interface - specify in Javadoc expected behavior
Implement tests • TFD: think of tests BEFORE implementing • define Stack<Integer> • assume "this.stack" is already instantiated • add @Before method to create the stack
implement Stack interface • write minimum! code to pass tests • if no test fails, we don't need to write code • passing code is valid Stack implementation. • additional classes require their test cases • Tip: override (and test) toString method, for all classes
Refactoring tests • test (positive) for push() • pop() to test push() – circular • pop() changes the state of the stack (removes top item)
Refactoring tests • Weak pop() test: - push an item and pop without exception • Better test: • returns last element pushed on the stack; • stack has one element less • … a copy of testPush() - one test for two functions • …complex post-conditions - rethink! - analyze contract of pop() • pop() does 2 things: • removes an item from the stack • returns the value of this item. • design (if possible) methods to do one thing - reusable, testable.
Refactoring tests:6 principles for writing testable interfaces 1. Separate commands and queries: • Queriesreturn a value and do not change the visible state of the object • methods with side-effect on the object - push() • Commands change the internal state of the object and do not return values • functions that only get information - isEmpty()
Refactor pop() • pop() does not stand by this rule: it is neither a command nor a query • replace pop() with primitive methods: • top() returns the value of the top object • remove() removes top object from the stack • keep pop(): • T pop() { T top = top(); remove(); return top; }
2. Separate Basic Queries from Derived Queries • Is this test strong enough? What could go wrong? • Change hats: assume person writing the code tries to pass the test with minimal effort • push=replace • testStackLIFO – 3 item stack
count() – add new query that indicates how many elements are stored in Stack • use to write post-conditionof push(), remove() • @pre(count()) - refer to value of count() before command is executed • isEmpty(): count()==0 • count() - primitive query • isEmpty() - derived query • post-condition computed based on primitive query
2. Separate basic and derived queries • Derived queries can be specified in terms of basic queries. 3. Define derived queries in terms of basic queries • Define the post-conditions of derived queries in terms of basic queries only
4. For each basic command, write post-conditions that specify values of basic queries • a command modifies the state of OUT • test modification, with basic queries: • review post-conditions of command by available basic queries of object • if no basic queries is affected – extend/revise interface • isEmpty() is a derived query (count()) – can be removed
Refactor remove() • contract encoded in the pre, post-conditions • remove() removes one element from stack • cannot call when the stack is empty. • WHICH element is removed (top)? • more specific contract • basic observe query
Did this make our post-condition for remove() stronger? • count() has been decreased by one • Do we need to check that the last element is the one that is removed? • itemAt for all values of i=1 to new count() are not affected • Did itemAt() break encapsulation? • stack limit access to only the top element
5. For each basic command and query, express pre-conditions in terms of basic queries • reminder: basic queries are sufficient to capture post-conditions of basic commands
6. Specify class invariants that impose “always true” constraints on basic queries • class invariants: properties that remain true in all legal states: @inv count() >= 0 • verify contract of commands that affect count() cannot break this invariant. • remove() : verify that the pre-condition prevents count() from changing from 0 to -1
Summary • specify the interface of objects before implement • interface specification includes contract for methods, expressed in terms of @pre, @post and @inv conditions. • write tests for interface to verify contract is enforced before implementation • design objects interface to be testable: • Separate commands and queries. • Separate basic queries and derived queries. • Define derived queries in terms of basic queries. • For each basic command, write post-conditions that specify values of basic queries • For each basic command and query, express the pre-conditions in terms of basic queries. • Specify class invariants that impose “always true” constraints on basic queries