240 likes | 459 Views
Testing and Debugging. Hakam Alomari halomari@cs.kent.edu. Outline. Include Guards Namespace name access Testing Concepts What is software testing? How to find faults? Black Box vs. Glass Box testing How many tests cases are there? Regression testing Using assert and cerr
E N D
Testing and Debugging HakamAlomari halomari@cs.kent.edu
Outline • Include Guards • Namespace name access • Testing Concepts • What is software testing? • How to find faults? • Black Box vs. Glass Box testing • How many tests cases are there? • Regression testing • Using assert and cerr • Building test cases, Makefile
Include Guards • Are used to prevent the contents of a file from being included more than once • Example: (string.h) #ifndef CS2_STRING_H #define CS2_STRING_H Class string { //… }; #endif
Example [wiki] File "grandfather.h“ structfoo { int member; }; File "father.h“ #include "grandfather.h" File "child.c“ #include "grandfather.h" #include "father.h"
Use of #include guards File "grandfather.h“ #ifndef GRANDFATHER_H #define GRANDFATHER_H structfoo { int member; }; #endif File "father.h“ #include "grandfather.h" File "child.c“ #include "grandfather.h" #include "father.h"
Namespaces • Used to group entities (e.g., objects) under a name, such as each group has its own name • Example: namespace foo1 { intvar = 1; } namespace foo2 { intvar = 2; } int main () { std::cout<< foo1::var <<“\n”; // output 1 std::cout<< foo2::var <<“\n”; // output 2 return 0; }
Standard Includes • Names defined by standard includes (e.g., cout) can be used as: • Prefix the name with std:: • Use using std::name before the name is used • Put using namespace::std before the name is used • Example: int main() { std::cout << "Hello World!\n"; // ... int main() { using std::cout; cout << "Hello World!\n"; // ... using namespace std; int main() { cout << "Hello World!\n"; // ...
Software Testing • Software testing is used to find errors • error: • Incorrect output for a given input • Caused by faults inside the program • A fault is represent the incorrect part of the code • E.g., incorrect stopping condition • So, test cases should be developed to exercise the program and uncovering errors
Testing levels • Start by testing each method (unit tests) • Then each class in full (module tests) • Then the whole program (system tests) • The test case that has a high probability to uncover an error is known to be the best one
Testing Types • Black box vs Glass box (white box) • Black box • Uses only the I/O spec. to develop test cases • Glass box • Uses only the implementation details to develop test cases • Both types are necessary to develop a good set of test cases
Number of Test Cases • Functions usually have a very large number of pre. and post. conditions • So, there is no need to test all of these to make sure our function behaves correctly • How? • Pairing down test cases, by: • Develop equivalence classes of test cases • Examine the boundaries of these classes carefully
Equivalence Classes • The input and output often fall into equivalence partitions where the system behaves in an equivalent way for each class • Then, the test cases should be developed to test each partition
Classes Boundaries • Example: • Partition system inputs and outputs into equivalence classes • If input is a 2 digit integer between 10 and 99 then the equivalence partitions are < 10 (invalid), 10 – 99 (valid), and > 99 (invalid) • Choose test cases at the boundary of these partitions • 09, 10, 99, 100
Building Test Cases • Determine the I/O spec. for the method • Develop test cases • Method implementation • Run the method against the test cases from 2 • Debugging (fix) • Go to 4
Test Steps • There are three steps in a test: • Set-up the "fixture", the system to be tested • Perform the test • Verify the results, make sure there are no unwanted side effects • Example: Test for existence of const char& operator[](int) const • // Setup fixture • const string str("abc"); • // Test • assert(str[0] == 'a'); • assert(str[1] == 'b'); • assert(str[2] == 'c'); • // Verify • assert(str.length() == 3);
Regression Testing • The intent of regression testing is to ensure that a change, such as a bugfix, did not introduce new faults • Each time you add a new method to your class or fix a fault run your test cases (all of them) • Adding something new or fixing a problem may have side effects • Re-running your test cases will uncover these problems
Asserts • Used to output an error message and terminate a program if a condition is false • Asserts are useful for testing • #include <cassert> is required to use asserts • Example: • assert(result == correct_result); • Asserts can be turned off using • #define NDEBUG
Debugging • Used when the program is not working properly • One easy thing to do is output the value of relevant variables • Use cerr instead of cout. cout is buffered • Buffering means output is accumulated in a "buffer" and the accumulated output sent out to the OS together • Output to cerr is not buffered, it is output immediately • If a program crashes there may be output in a buffer that gets lost • It is a good idea to include a message with values that are output • std::cerr << "var1 = " < < var1 << "\n";
The make Command • The make command may be used to automate compiling • The make command when executed will look for a file named makefile and then, if not found, for a file named Makefile to specify dependencies • Otherwise a makefile must be specified with the -f option • The make command has some rules built-in so it can do some basic compiling but usually the developer provides a makefile
Makefile Rules • Rules have the form: • target: dependency_list • TAB command • Target A file or name • dependency_list Files that the target "depends on" • TAB The TAB character. REQUIRED! • Command Typically a compiling/linking command but can be any command. There may be more than one TAB/command line • If the modification time of any dependency is more recent than the target the command lines are executed
Common Rule Types • Creating machine language, .o, files and linking are two things that must be done # Link executable: file_1.o file_2.o g++ -Wall file_1.o file_2.o -o executable # Create .o file file.o: file.h file.cpp g++ -Wall -c file.cpp • Note .cpp files are typically not targets, .cpp files do not depend on other files
Makefile Example # Create the executable is the first rule here # Link main: bigint.omain.o g++ -Wall bigint.omain.o -o main # Create .o file main.o: bigint.h main.cpp g++ -Wall -c main.cpp # Create .o file bigint.o: bigint.h bigint.cpp g++ -Wall -c bigint.cpp # Remove .o and executable clean: rm -f *.o rm -f main
References • [wiki]: http://en.wikipedia.org/wiki/Include_guard • Computer Science II Lab: http://classes.cs.kent.edu/courses/cs33001_lab/svn/index.html • Software Testing for CS II slides: http://www.cs.kent.edu/~jmaletic/CS33001/