Systematic Software Testing Using Test Abstractions
Download
Report
Transcript Systematic Software Testing Using Test Abstractions
Systematic Software Testing
Using Test Abstractions
Darko Marinov
CS527: Topics in Software Engineering
August 31, 2010
Why Testing?
Goal: Increase software reliability
Approach: Find bugs using testing
Estimated savings from better testing $22B/year
Challenge: Manual testing is problematic
Software bugs cost US economy $60B/year [NIST 2002]
Time-consuming, error-prone, expensive
Research: Automate testing
Reduce cost, increase benefit
My Group’s Research on Testing
Sequential code
Milos Gligoric: Code with structurally complex inputs
Brett Daniel: OO unit test generation and repair
Multithreaded code
Vilas Jagannath: Incremental testing of parallel code
Adrian Nistor: HW support for testing of parallel code
Message-passing code
Steve Lauterburg: Systematic exploration of message
schedules, dynamic partial-order reduction
Examples of Structurally Complex Data
red-black tree
web sites
s2
1
3
0
2
XML document
<library>
<book year=2009>
<title>T1</title>
<author>A1</author>
</book>
<book year=2010>
<title>T2</title>
<author>A2</author>
</book>
</library>
/library/book[@year<2010]/title
s1
s4
s5
s3
Java program
class A {
int f;
}
class B extends A {
void m() {
super.f = 0;
}
}
Running Example: Inheritance Graphs
Java
program
Inheritance
graph
interface I {
void m();
}
I
J
interface J {
void m();
}
class A implements I {
void m() {}
}
class B extends A
implements I,J {
void m() {}
}
Properties
1. DAG
A
2. ValidJava
B
.
Testing Setup
inputs
outputs
1
2
0
3
0
3
2
test
generation
test
oracle
code
3
2
0
pass
3
0
fail
Examples of code under test
Compiler or refactoring engines, input: programs
Abstract data type, input/output: data structure
Web traversal code, input: web topology
XML processing code, input/output: XML document
Test Generation
Manual
Fully automatic
inputs
inputs
1
1
0
0
3
3
2
2
tool
code
2
2
0
0
3
3
inputs
Semi automated
1
0
3
2
input set
description
tool
2
0
3
Test Abstractions
Each test abstraction describes a set of
(structurally complex) test inputs
Example: inheritance graphs with up to N nodes
Workflow
Tester manually writes test abstractions
Tool automatically generates test inputs
Benefits
No need to manually write large number of test inputs
Useful for test generation, maintenance, and reuse
Key Questions for Test Abstractions
inputs
1
0
3
2
test
abstraction
tool
2
0
2. What language to use
for test abstractions?
3
1. Which test inputs
to
generate?
3. How to generate test inputs
from abstractions?
4. How to check test outputs?
5. How to map failures to faults?
Which Inputs to Generate?
Bounded-exhaustive testing
Generate all inputs within given (small) bounds
Rationale: small-scope hypothesis [Jackson & Damon ISSTA’96]
Found bugs in both academia and industry
Java compilers, model checkers… [Gligoric et al. ICSE’10]
Refactoring engines in Eclipse/NetBeans [Daniel et al. FSE’07]
Web traversal code from Google [Misailovic et al. FSE’07]
XPath compiler at Microsoft [Stobie ENTCS’05]
Fault-tree analyzer for NASA [Sullivan et al. ISSTA’04]
Constraint solver, network protocol [Khurshid & Marinov J-ASE’04]
How to Describe/Generate Inputs?
Filtering test abstractions [SOFTMC’01, ASE’01, FME’02, ISSTA’02,
OOPSLA’02, SAT’03, MIT-TR’03, J-ASE’04, SAT’05, LDTA’06, ALLOY’06, ICSE-DEMO’07, STEP’07,
FSE’07, ISSTA’08, ICST’09, CSTVA’10]
filters
bounds
search
input
s
Testers
describe
what properties
test inputs satisfy
Tool:
search
Generating test abstractions [FSE’07, FASE’09]
generators
bounds
execute
input
s
Testers
describe
how to generate
test inputs
Tool:
execution
Previously Explored Separately
Inheritance
graph
Other
Java
properties
filters
generators
DAG
X
ValidJava
X
property 3
X
…
…
…
property N
X
Some properties easier/harder as filters/generators
Key challenge for combining: search vs. execution
UDITA: Combined Both [ICSE’10]
Inheritance
graph
Other
Java
properties
filters
generators
DAG
X
ValidJava
X
property 3
X
…
…
…
property N
X
Extends Java with non-deterministic choices
Found bugs in Eclipse/NetBeans/javac/JPF/UDITA
Outline
Introduction
Example
Filtering Test Abstractions
Generating Test Abstractions
UDITA: Combined Filtering and Generating
Evaluation
Conclusions
Example: Inheritance Graphs
class IG {
Node[ ] nodes;
Properties
int size;
DAG
static class Node {
Node[ ] supertypes;
boolean isClass;
}
}
Nodes in the graph should
have no direct cycle
ValidJava
Each class has at most one
superclass
All supertypes of interfaces
are interfaces
Example Valid Inheritance Graphs
G1
G2
I
C
G4
J
J
G3
G5
C
A
I
J
A
D
B
B
C
A
B
C
Example Invalid Inheritance Graphs
G1
G2
I
C
G4
J
J
G3
cycle
G5
C
A
I
J
A
D
B
B
class
implements
class
C
class
extends two
classes
interface
extends
class
A
B
class
extends
interface
C
(In)valid Inputs
Valid inputs = desired inputs
Inputs on which tester wants to test code
Code may produce a regular output or an exception
E.g. for Java compiler
Interesting (il)legal Java programs
No precondition, input can be any text
E.g. for abstract data types
Encapsulated data structure representations
Inputs need to satisfy invariants
Invalid inputs = undesired inputs
Outline
Introduction
Example
Filtering Test Abstractions
Generating Test Abstractions
UDITA: Combined Filtering and Generating
Evaluation
Conclusions
Filtering Test Abstractions
Tool requires:
Filters: encode what properties inputs satisfy
Bounds
Tool provides:
All inputs within bounds that satisfy the properties
filters
bounds
search
input
s
Language for Filters
Each filter takes an input that can be valid or invalid
Returns a boolean indicating validity
Experience from academia and industry showed
that using a new language makes adoption hard
Write filters in standard implementation language
(Java, C#, C++…)
Advantages
Familiar language
Existing development tools
Filters may be already present in code
Challenge: generate inputs from imperative filters
Example Filter: DAG Property
boolean isDAG(IG ig) {
Set<Node> visited = new HashSet<Node>();
Set<Node> path = new HashSet<Node>();
if (ig.nodes == null || ig.size != ig.nodes.length)
return false;
for (Node n : ig.nodes)
if (!visited.contains(n))
if (!isAcyclic(n, path, visited)) return false;
return true;
}
boolean isAcyclic(Node node, Set<Node> path,
Set<Node> visited) {
if (path.contains(node)) return false;
path.add(node); visited.add(node);
for (int i = 0; i < supertypes.length; i++) {
Node s = supertypes[i];
for (int j = 0; j < i; j++)
if (s == supertypes[j]) return false;
if (!isAcyclic(s, path, visited)) return false;
}
path.remove(node);
return true;
}
Input Space
All possible object graphs with an IG root
(obeying type declarations)
Natural input spaces relatively easy to enumerate
Sparse: # valid test inputs << # all object graphs
0
3
0
3
2
0
3
0
0
3
0
1
2
3
0
3
3
0
0
3
0
3
3
0
3
0
3
Bounded-Exhaustive Generation
Finite (small) bounds for input space
Generate all valid inputs within given bounds
Number of objects
Values of fields
Eliminates systematic bias
Finds all bugs detectable within bounds
Avoid equivalent (isomorphic) inputs
Reduces the number of inputs
Preserves capability to find all bugs
Example Bounds
Specify number of objects for each class
1 object for IG: { IG0 }
3 objects for Node: { N0, N1, N2 }
Specify set of values for each field
For size: { 0, 1, 2, 3 }
For supertypes[i]: { null, N0, N1, N2 }
For isClass: { false, true }
Previously written with special library
In UDITA: non-deterministic calls for initialization
Bounds Encoded in UDITA
Java with a few extensions for non-determinism
IG initialize(int N) {
IG ig = new IG(); ig.size = N;
ObjectPool<Node> pool = new ObjectPool<Node>(N);
ig.nodes = new Node[N];
for (int i = 0; i < N; i++) ig.nodes[i] = pool.getNew();
for (Node n : nodes) {
int num = getInt(0, N − 1);
n.supertypes = new Node[num];
static void mainFilt(int N) {
for (int j = 0; j < num; j++)
IG ig = initialize(N);
n.supertypes[j] = pool.getAny();
assume(isDAG(ig));
n.isClass = getBoolean(); }
assume(isValidJava(ig));
println(ig); }
return ig; }
Example Input Space
1 IG object, 3 Node objects: total 14 fields
(“nodes” and array length not shown)
IG0
size
3
N0
s[0] s[1] isC
N1
s[0] s[1] isC
N2
s[0] s[1] isC
N1
null null
null null
N1
2
1
3
0
null null false
null null false
null null false
1
N0
N0
N0
N0
N0
N0
N1
N1
N1
N1
N1
N1
N2
N2
N2
N2
N2
N2
2
3
true
true
true
4 * (4 * 4 * 2 * 3)3 > 219 inputs, but < 25 valid
Input Generation: Search
Naïve “solution”
Enumerate entire (finite) input space, run filter on
each input, and generate input if filter returns true
Infeasible for sparse input spaces (#valid << #total)
Must reason about behavior of filters
Our previous work proposed a solution
Dynamically monitors execution of filters
Prunes large parts of input space for each execution
Avoids isomorphic inputs
Outline
Introduction
Example
Filtering Test Abstractions
Generating Test Abstractions
UDITA: Combined Filtering and Generating
Evaluation
Conclusions
Generating Test Abstractions
Tool provides:
Generators: encode how to generate inputs
Bounds
Tool requires:
All inputs within bounds (that satisfy the properties)
generators
bounds
execute
input
s
Example Generator: DAG Property
N0
N1
N2
void mainGen(int N) {
IG ig = initialize(N);
generateDAG(ig);
generateValidJava(ig);
println(ig); }
void generateDAG(IG ig) {
for (int i = 0; i < ig.nodes.length; i++) {
int num = getInt(0, i);
ig.nodes[i].supertypes = new
Node[num];
for (int j = 0, k = −1; j < num; j++) {
k = getInt(k + 1, i − (num − j));
ig.nodes[i].supertypes[j] = ig.nodes[k];
}
}
}
Outline
Introduction
Example
Filtering Test Abstractions
Generating Test Abstractions
UDITA: Combined Filtering and Generating
Evaluation
Conclusions
Filtering vs. Generating: DAG Property
boolean isDAG(IG ig) {
Set<Node> visited = new HashSet<Node>();
Set<Node> path = new HashSet<Node>();
if (ig.nodes == null || ig.size != ig.nodes.length)
return false;
for (Node n : ig.nodes)
if (!visited.contains(n))
if (!isAcyclic(n, path, visited)) return false;
return true;
}
boolean isAcyclic(Node node, Set<Node> path,
Set<Node> visited) {
if (path.contains(node)) return false;
path.add(node); visited.add(node);
for (int i = 0; i < supertypes.length; i++) {
Node s = supertypes[i];
for (int j = 0; j < i; j++)
if (s == supertypes[j]) return false;
if (!isAcyclic(s, path, visited)) return false;
}
path.remove(node);
return true;
}
void generateDAG(IG ig) {
for (int i = 0; i < ig.nodes.length; i++) {
int num = getInt(0, i);
ig.nodes[i].supertypes = new Node[num];
for (int j = 0, k = −1; j < num; j++) {
k = getInt(k + 1, i − (num − j));
ig.nodes[i].supertypes[j] = ig.nodes[k];
}
}
}
Filtering arguably harder
than generating for DAG
20+ vs. 10 LOC
However, the opposite
for ValidJava property
UDITA Requirement: Allow Mixing
filters
generators
DAG
X
ValidJava
X
filters
generators
bounds
UDITA
input
s
Other Requirements for UDITA
Language for test abstractions
Ease of use: naturally encode properties
Expressiveness: encode a wide range of properties
Compositionality: from small to large test abstractions
Familiarity: build on a popular language
Algorithms and tools for efficient generation of
concrete tests from test abstractions
Key challenge: search (filtering) and execution
(generating) are different paradigms
UDITA Solution
Based on a popular language (Java) extended with
non-deterministic choices
Base language allows writing filters for search/filtering
and generators for execution/generating
Non-deterministic choices for bounds and enumeration
Non-determinism for primitive values: getInt/Boolean
New language abstraction for objects: ObjectPool
class ObjectPool<T> {
public ObjectPool<T>(int size, boolean includeNull) { ... }
public T getAny() { ... }
public T getNew() { ... } }
UDITA Implementation
Implemented in Java PathFinder (JPF)
Explicit state model checker from NASA
JVM with backtracking capabilities
Has non-deterministic call: Verify.getInt(int lo, int hi)
Default/eager generation too slow
Efficient generation by delayed choice execution
Publicly available JPF extension
http://babelfish.arc.nasa.gov/trac/jpf/wiki/projects/jpf-delayed
Documentation on UDITA web page
http://mir.cs.illinois.edu/udita
Delayed Choice Execution
Postpone choice of values until needed
Eager
int x = getInt(0, N);
…
y = x;
non-determinism
…
… = y; // non-copy
Delayed
int x = Susp(0, N);
…
y = x;
…
force(y);
… = y; // non-copy
Lightweight symbolic execution
Avoids problems with full blown symbolic execution
Even combined symbolic and concrete execution has
problems, especially for complex object graphs
Object Pools
Eager implementation of getNew/getAny by
reduction to (eager) getInt
Simply making getInt delayed does not work because
getNew is stateful
We developed a novel algorithm for delayed
execution of object pools
Gives equivalent results as eager implementation
Polynomial-time algorithm to check satisfiability of
getNew/getAny constraints (disequality from a set)
Previous work on symbolic execution typically encodes into
constraints whose satisfiability is NP-hard (disjunctions)
Outline
Introduction
Example
Filtering Test Abstractions
Generating Test Abstractions
UDITA: Combined Filtering and Generating
Evaluation
Conclusions
Evaluation
UDITA
Compared Delayed and Eager execution
Compared with a previous Generating approach
Compared with Pex (white-box)
Case studies on testing with test abstractions
Some results with filtering test abstractions
Some results with generating test abstractions
Tested refactoring engines in Eclipse and NetBeans,
Java compilers, JPF, and UDITA
Eager vs. Delayed
.
Generating vs. UDITA: LOC
Generating vs. UDITA: Time
UDITA vs. Pex
Compared UDITA with Pex
State-of-the-art symbolic execution
engine from MSR
Uses a state-of-the-art constraint solver
Comparison on a set of data structures
White-box testing: tool monitors code under test
Finding seeded bugs
Result: object pools help Pex to find bugs
Summer internship to include object size in Pex
Some Case Studies with Filtering
Filtering test abstractions used at Microsoft
Enabled finding numerous bugs in tested apps
XML tools
Web-service protocols
XPath compiler (10 code bugs, test-suite augmentation)
Serialization (3 code bugs, changing spec)
WS-Policy (13 code bugs, 6 problems in informal spec)
WS-Routing (1 code bug, 20 problems in informal spec)
Others
SSLStream
MSN Authentication
Some Case Studies with Generating
Generating test abstractions used to test Eclipse
and NetBeans refactoring engines [FSE’07]
Eight refactorings: target field, method, or class
Wrote about 50 generators
Reported 47 bugs
21 in Eclipse: 20 confirmed by developers
26 in NetBeans: 17 confirmed, 3 fixed, 5 duplicates,
1 won't fix
Found more but did not report duplicate or fixed
Parts of that work included in NetBeans
Typical Testing Scenario
Create a model of test inputs (e.g., Java ASTs)
Write filters/generators for valid inputs
UDITA generates valid inputs
Translate from model to actual inputs (pretty-print)
Run on two code bases, compare outputs
filters/generators
UDITA
bounds
pretty
printer
one
code
<library>
<book year=2001>
<title>T1</title>
<author>A1</author>
</book>
<book year=2002>
<title>T2</title>
<author>A2</author>
</book>
<book year=2003>
<title>T3</title>
<author>A3</author>
</book>
</library>
/library/book[@year<2003]/titl
another
code
pass
<title>T1</title>
<title>T2</title>
=?
<title>T1</title>
<title>T2</title>
fail
Some More Bugs Found
Eclipse vs. NetBeans refactoring engines
Sun javac compiler vs. Eclipse Java compiler
2 reports (still unconfirmed) for Sun javac
Java PathFinder vs. JVM
2 bugs in Eclipse and 2 bugs in NetBeans
6 bugs in JPF (plus 5 more in an older version)
UDITA Delayed vs. UDITA Eager
Applying tool on (parts of) itself
1 bug in UDITA (patched since then)
Example Bug
Generated program
Incorrect refactoring
import java.util.List;
class A implements B, D {
public List m() {
List l = null;
A a = null;
l.add(a.m());
return l; } }
import java.util.List;
class A implements B, D {
public List<List> m() {
List<List<List>> l = null;
A a = null;
l.add(a.m());
return l; } }
interface D {
public List m();
}
interface D {
public List<List> m();
}
interface B extends C {
public List m();
}
interface B extends C {
public List<List> m();
}
interface c {
public List m();
}
interface c {
public List<List> m();
}
Outline
Introduction
Example
Filtering Test Abstractions
Generating Test Abstractions
UDITA: Combined Filtering and Generating
Evaluation
Conclusions
Summary
Testing is important for software reliability
Necessary step before (even after) deployment
Structurally complex data
Increasingly used in modern systems
Important challenge for software testing
Test abstractions
Proposed model for complex test data
Adopted in industry
Used to find bugs in several real-world applications
Benefits & Costs of Test Abstractions
Benefits
Test abstractions can find bugs in real code (e.g., at
Microsoft or for Eclipse/NetBeans/javac/JPF/UDITA)
Arguably better than manual testing
Costs
1. Human time for writing test abstractions (filters or
generators) [UDITA, ICSE’10]
2. Machine time for generating/executing/checking tests
(human time waiting for the tool) [FASE’09]
3. Human time for inspecting failing tests [FASE’09]
Credits for Generating TA and UDITA
Collaborators from U. of Illinois and other schools
Brett Daniel
Milos
Danny Dig
Tihomir
Gvero (Belgrade->EPFL)
Kely Garcia
Sarfraz
Khurshid (UT Austin)
Vilas Jagannath
Viktor
Yun Young Lee
Gligoric (Belgrade->Illinois)
Kuncak (EPFL)
Funding
NSF CCF-0746856, CNS-0615372, CNS-0613665
Microsoft gift
Test Abstractions
Test abstractions
Proposed model for structurally complex test data
Adopted in industry
Used to find bugs in several real-world applications
(e.g., at Microsoft, Eclipse/NetBeans/javac/JPF/UDITA)
UDITA: latest approach for test abstractions
Easier to write test abstractions
Novel algorithm for faster generation
Publicly available JPF extension
http://mir.cs.illinois.edu/udita