Static code analysis |
Example-based testing Given inputs should produce given results |
Proved code Demonstrate a code by checking that its invariants hold throughout its execution |
Crash under random Monitor a software for exceptions on random inputs - Monkey testing - or random data - Fuzzing |
Test properties instead of isolated cases
for all (x, y, ...)
such that precondition(x, y, ...) holds
property(x, y, ...) is true
for all run it 100 times
(x, y, ...) generate random inputs based on specified generators
such that
precondition(x, y, ...) holds
check pre-conditions
fails?: go back to previous
property(x, y, ...) is true
run the test
fails?: try shrinking
Let's suppose that we want to test is_substring
for all a, b, c strings
b is a substring of a + b + c
Let's suppose that our implementation fails when the string ends by the pattern (arbitrary choice)
Framework might have built:
(a, b, c) = ("aaa", "bbb", "ccc")
is_substring("bbb", "aaabbbccc")? TEST OK
(a, b, c) = ( "", "dd", "e")
is_substring("dd", "dde")? TEST OK
(a, b, c) = ( "yy", "w", "")
is_substring("w", "yyw")? TEST FAILURE
-- SHRINK INPUT --
(a, b, c) = ( "", "", "")
is_substring("", "")? TEST FAILURE
A helper not an end in itself
But what if it was:
Converts a value to JSON notation representing it.
Such specification is the one described by Mozilla Foundation web docs for `JSON.stringify` of JavaScript. The exhaustive specification is available on ECMA-262.
namespace json {
class JsonObject;
class JsonArray : public JsonObject;
class JsonDictionary : public JsonObject;
std::string stringify(JsonObject const&);
std::unique_ptr<JsonObject> parse(std::string const&);
}//json
TEST(Stringify, ObjectWithTwoAttributesAandB) {
auto instanceWithAandB = json::JsonDictionary {}
.with("a", json::JsonInteger {4})
.with("b", json::JsonInteger {7});
ASSERT_EQ(
std::string{"{\"a\":4,\"b\":7}"},
json::stringify(instanceWithAandB));
}
Key ordering
Change storage strategy? |
DeterministicRun on other platform? Static data? |
Indent
Divide into multiple threads? |
Notation
Full unicode? |
namespace json {
std::string stringify(JsonObject const&);
std::unique_ptr<JsonObject> parse(std::string const&);
}//json
RC_GTEST_PROP(Stringify,
StringifyThenParseIsIdentity,
(json::JsonObject const& elt))
{
RC_ASSERT(*json::parse(json::stringify(elt)) == elt);
}
Taking the object resulting from the conversion of the JsonObject to a string then back to a JsonObject should be equivalent to take the original instance itself.
Two sets of tests:
(0,0)
Hint:
$: dimensions = (8, 8)
$: position = (4, 4)
$: hint = "DR"
Hint:
$: dimensions = (8, 8)
$: position = (4, 4)
$: hint = "DR"
Hint:
$: dimensions = (8, 8)
$: position = (6, 6)
$: hint = "D"
Hint:
$: dimensions = (8, 8)
$: position = (6, 6)
$: hint = "D"
Hint:
$: dimensions = (8, 8)
$: position = (6, 7)
$: hint = ""
void find_bomb(Game& game);
class Game
{
public:
// dimensions of the grid
std::size_t dimension_x() const;
std::size_t dimension_y() const;
// last known location
std::size_t previous_x() const;
std::size_t previous_y() const;
// direction to the target (U / D, L / R)
std::string const& hint() const;
// is it the position of the target?
bool solved() const;
// update position
void move(std::size_t x, std::size_t y);
};
Hint: DR
Hint: DR
Hint: U
Hint: U
...
void find_bomb(Game& game)
{
std::size_t x_min = 0;
std::size_t x_max = game.dimension_x();
std::size_t y_min = 0;
std::size_t y_max = game.dimension_y();
while (x_min < x_max && y_min < y_max)
{
// content on next slide
}
}
std::size_t x0 = game.previous_x();
auto const& hint = game.hint();
if (hint.back() == 'L')
{
x_max = x0 -1;
x0 = (x_max + x_min) /2;
}
else if (hint.back() == 'R')
{
x_min = x0 +1;
x0 = (x_max + x_min) /2;
}
// same for y...
game.move(x0, y0);
Solve the game in a limited amount of guesses
At first, we can assume that we will be able to solve it in a maximum of width x height
We can solve it in max(width, height) without too many complexity
We can solve it in ceil(log2(max(width, height)))
(width, height): integers between 1 and 10 000*
(start_x, target_x): integers between 0 and width*
(start_y, target_y): integers between 0 and height*
(num_rounds): max(width, height)
*not included
Reach the target with a maximum of num_rounds rounds
RC_GTEST_PROP(ShadowsOfTheKnight, SAlwaysReachTarget, ())
{
auto w = *inRange(1, 10000).as("grid width");
auto h = *inRange(1, 10000).as("grid height");
auto inRangeGen = apply(
[](auto x, auto y) { return std::make_pair(x, y); }
, inRange(0, w), inRange(0, h));
auto current = *inRangeGen.as("start position");
auto solution = *inRangeGen.as("target position");
TestGame game = TestGameBuilder{}
.withDimension(w, h)
.withSolution(solution.first, solution.second)
.withCurrent(current.first, current.second).build();
find_bomb(game);
RC_ASSERT(game.solved());
}
Falsifiable after 9 tests and 5 shrinks
grid width:
8
grid height:
8
start position:
(0, 1)
target position:
(1, 0)
Hint: Next move:
$: dimensions = (8, 8) $: xmin, xmax = (1, 8)
$: xmin, xmax = (0, 8) $: ymin, ymax = (0, 0)
$: ymin, ymax = (0, 8) $: position = (4, 0)
$: position = (0, 1)
$: hint = "UR"
Hint: Next move:
$: dimensions = (8, 8) $: Cannot suggest anything,
$: xmin, xmax = (1, 8) $: it seems that
$: ymin, ymax = (0, 0) $: there is no solution
$: position = (4, 0)
$: hint = "L"
Hint: Next move:
$: dimensions = (8, 8) $: xmin, xmax = (1, 3)
$: xmin, xmax = (1, 8) $: ymin, ymax = (0, 1)
$: ymin, ymax = (0, 1) $: position = (2, 0)
$: position = (4, 0)
$: hint = "L"
Hint: Next move:
$: dimensions = (8, 8) $: xmin, xmax = (1, 2)
$: xmin, xmax = (1, 3) $: ymin, ymax = (0, 1)
$: ymin, ymax = (0, 1) $: position = (1, 0)
$: position = (2, 0)
$: hint = "L"
Hint:
$: dimensions = (8, 8)
$: xmin, xmax = (1, 2)
$: ymin, ymax = (0, 1)
$: position = (1, 0)
$: hint = ""
Let's test a web interface
We are already able to:
Characteristics of a UI under test:
We need to define commands:
We have to produce arrays of commands
commands = array of commands in {CommandA, CommandB, ...}
Then give them to the framework
List of commands we would like to test:
Open color picker:
var OpenPaletteForCommand = function(colorId) {
// can we run the command?
this.check = model => is_picker_accessible(model, colorId);
// run it
this.run = function(browser, model) {
model.picker_for = colorId;
model.color_before = await read_color(browser, colorId);
await open_picker(browser, model, colorId);
model.color_selected = await read_color_picker(browser);
return true;
};
};
Cancel modification:
var CancelCommand = function() {
// can we run the command?
this.check = model => is_picker_opened(model);
// run it
this.run = function(browser, model) {
await cancel_picker(browser);
const color_after_cancel = await read_color(browser, model.picker_for);
return model.color_before == color_after_cancel;
};
};
- Expected Error: Failed after 1 tests and 10 shrinks.
rngState: 00a5eb241093fc6d78;
Counterexample: ;
GoToPanelCommand(color),
OpenPaletteForCommand(palette_NEUTRAL_DARK),
AlphaSpectrumCommand(alpha:2%),
CancelPaletteCommand();
to be undefined.
This example is inspired by a real case. The issue with cancel only occured when we were starting our modification by impacting alpha. It did not failed when we touched spectrum or hue followed by alpha and canceled just after.
Talk materials & sources - dubzzz