| Lecture 5: Examples and Tests | ||||||||||||||||||
Interfaces: Purpose and ContractThus far, we have learned how to scale up the first two steps of the design recipe to larger projects. First, we analyze the problem with concept graphs and use cases. Second, we excerpt interfaces from these. An interface is a description of some actor or piece of data (what it represents) and what kind of actions we expect to happen on them. Each action consumes inputs and produces outputs.For example, we may be working on a piece of software that deals with orders:
The little excerpt here identifies four concepts: a customer, an order, an
orderline, and the total cost of an order.
A simple use case may proceed like this. The customer opens an order, adds several "lines" to the order, and then demands to know the total cost of the order. The use case suggests that Orders are an important class of data in our program. Furthermore, we can see three important actions: opening an order, adding a line to the order, and adding up the total. Without even committing to a programming language, we can write down what this means in some reasonably rigorous way:
We can take this as an English description for a piece of code in a procedural,
untyped language such as Scheme or we can easily turn into a piece of Java
code. In either case, we have names for functionality we must implement, and we
can finally begin to code.
ExamplesWell, almost. If you remember the design recipe, you know that we really need to formulate examples and turn them into tests. To formulate examples, we still don't need to choose a language; it is doable at the conceptual level.Given: an empty order Given: an order with one line, for a single product with price $99.99 Making up examples for programs, small or large, helps us understand how the software is supposed to work and helps us formulate criteria for when we want to accept/reject code. Ideally, we make this process as automatic as possible so that we can apply it repeatedly and to intermediate solutions, just to conduct a reality check. Tests FirstFor this reason, we translate the examples into executable tests. A unit test is a piece of code that exercises a unit of code and reports how the actual behavior of the code compares with the expected behavior. It is therefore imperative that a test specifies the expected result, the piece of program to be run (with input and proper setup), and a mechanism for comparing the expected outcome with the actual outcome.A customer test (blackbox test) runs a complete program without any knowledge about its interior construction and ensures that an entire use case works as expected. Testing FrameworksIn this day, most programming languages come with testing frameworks that help programmers formulate tests before they work on the code. For example, Java comes with jUnit, PLT Scheme comes with SchemeUnit (see planet.plt-scheme.org).For illustrative purposes, let us look at how a "design recipe programmer"
would develop the code for
Using SchemeUnit, we can then express the above examples as follows:
Later when we want to run the test cases, we form a test suite (a collection of independent test cases) and ask for a report:
Of course, we can't run this test suite without completing the definition of
getTotal but at least we are now ready to do so.
Here is what happens when we complete the definition in a bad way and run the test suite:
We see that one test case succeeded, one failed and where/why it failed.
Tests for Imperative ProgrammingWhen someone has designed the functions or methods of our interfaces such that they interact via effects on variables, testing becomes more complicated but it is still imperative to do so.Consider the example of a queue whose interfaces demands changes to its internal state:
In particular, enq puts some item away and deq
retrieves it and removes it from that secret place. Also, sze is a
function that makes an observation about this hidden piece of data, but doesn't
reveal its exact nature.
A test of these methods should, for example, ensure that
To accomplish this second goal, we need to add setup code that
performs the enq operation and that happens before the assertion is
evaluated. To make sure that other test cases work independently of this one, we
also add teardown code that undoes the effects of the
setup code.
A final consideration concerns the testing of exceptional behavior. A queue,
for example, may raise an exception when a client tries to
This implementation of queue<%> stores its items in a list, and if
this list is empty when deq is called, the method throws an
exception.
To test this behavior requires a slightly different setup:
More precisely, the test case must specify that an exception is expected, and
when none is thrown, it will report this behavior as erroneous.
In summary, before you program find out what your language supports in terms of test suite languages and read up on its capabilities. If it doesn't, take the time to implement a minial framework yourself. The two references show that this isn't really difficult. Bibliography |
last updated on Tue Jun 9 22:03:19 EDT 2009 | generated with PLT Scheme |