Skip to content

Commit

Permalink
Model based testing example
Browse files Browse the repository at this point in the history
  • Loading branch information
dubzzz committed Jun 13, 2018
1 parent f5ca06a commit b80b4f9
Show file tree
Hide file tree
Showing 6 changed files with 183 additions and 3 deletions.
47 changes: 47 additions & 0 deletions example/model-based-testing/MusicPlayer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/**
* Basic state machine
* Example of how fast-check can be derived for model based testing
*/
export class MusicPlayer {
private tracks: string[];
private isPlaying: boolean;
private playingId: number;

constructor(tracks: string[]) {
this.tracks = [...tracks];
this.isPlaying = false;
this.playingId = 0;
}

playing(): boolean {
return this.isPlaying;
}

currentTrackName(): string | null {
return this.playingId != null && this.playingId < this.tracks.length ? this.tracks[this.playingId] : null;
}

play(): void {
this.isPlaying = true;
}
pause(): void {
this.isPlaying = false;
}

addTrack(trackName: string, position: number): void {
this.tracks = [...this.tracks.slice(0, position), trackName, ...this.tracks.slice(position)];
if (this.playingId >= position) {
// comment this block to check bug detection
++this.playingId;
}
}

next(): void {
if (++this.playingId === this.tracks.length) {
this.playingId = 0;
}
}
jump(position: number): void {
this.playingId = position;
}
}
12 changes: 12 additions & 0 deletions example/model-based-testing/helpers/Command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export interface Command<Model, Real> {
// Check if the model is in the right state to apply the command
// WARNING: does not change the model
checkPreconditions(m: Model): void;

// Apply the command on the model
apply(m: Model): void;

// Receive the non-updated model and the real or system under test
// Performs the checks post-execution - Throw in case of invalid state
run(m: Model, r: Real): void;
}
12 changes: 12 additions & 0 deletions example/model-based-testing/helpers/CommandExecutor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Command } from './Command';

type Setup<Model, Real> = () => { model: Model; real: Real };
export const CommandExecutor = <Model, Real>(s: Setup<Model, Real>, cmds: Command<Model, Real>[]): void => {
const { model, real } = s();
for (const c of cmds) {
if (c.checkPreconditions(model)) {
c.run(model, real);
c.apply(model);
}
}
};
108 changes: 108 additions & 0 deletions example/model-based-testing/test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import * as assert from 'assert';
import * as fc from 'fast-check';
import { Command } from './helpers/Command';
import { CommandExecutor } from './helpers/CommandExecutor';

import { MusicPlayer } from './MusicPlayer';

class MusicPlayerModel {
isPlaying: boolean = false;
numTracks: number = 0;
tracksAlreadySeen: { [Key: string]: boolean } = {}; // our model forbid to append twice the same track
}
type MusicPlayerCommand = Command<MusicPlayerModel, MusicPlayer>;

class PlayCommand implements MusicPlayerCommand {
checkPreconditions(m: MusicPlayerModel) {
return true;
}
apply(m: MusicPlayerModel) {
m.isPlaying = true;
}
run(m: MusicPlayerModel, p: MusicPlayer) {
p.play();
assert.ok(p.playing());
}
toString() {
return 'Play';
}
}
class PauseCommand implements MusicPlayerCommand {
checkPreconditions(m: MusicPlayerModel) {
return true;
}
apply(m: MusicPlayerModel) {
m.isPlaying = false;
}
run(m: MusicPlayerModel, p: MusicPlayer) {
p.pause();
assert.ok(!p.playing());
}
toString() {
return 'Pause';
}
}
class NextCommand implements MusicPlayerCommand {
checkPreconditions(m: MusicPlayerModel) {
return true;
}
apply(m: MusicPlayerModel) {
/**/
}
run(m: MusicPlayerModel, p: MusicPlayer) {
const trackBefore = p.currentTrackName();
p.next();
assert.equal(p.playing(), m.isPlaying);
if (m.numTracks === 1) {
assert.equal(p.currentTrackName(), trackBefore);
} else {
assert.notEqual(p.currentTrackName(), trackBefore);
}
}
toString() {
return 'Next';
}
}
class AddTrackCommand implements MusicPlayerCommand {
constructor(readonly position: number, readonly trackName: string) {}
checkPreconditions(m: MusicPlayerModel) {
return !m.tracksAlreadySeen[this.trackName];
}
apply(m: MusicPlayerModel) {
++m.numTracks;
m.tracksAlreadySeen[this.trackName] = true;
}
run(m: MusicPlayerModel, p: MusicPlayer) {
const trackBefore = p.currentTrackName();
p.addTrack(this.trackName, this.position % (m.numTracks + 1)); // old model
assert.equal(p.playing(), m.isPlaying);
assert.equal(p.currentTrackName(), trackBefore);
}
toString() {
return `AddTrack(${this.position}, ${this.trackName})`;
}
}

describe('MusicPlayer', () => {
const TrackNameArb = fc.hexaString(1, 10);
const CommandsArb: fc.Arbitrary<MusicPlayerCommand[]> = fc.array(
fc.oneof(
fc.constant(new PlayCommand()),
fc.constant(new PauseCommand()),
fc.constant(new NextCommand()),
fc.record({ position: fc.nat(), trackName: TrackNameArb }).map(d => new AddTrackCommand(d.position, d.trackName))
)
);
it('should run fast-check on model based approach', () =>
fc.assert(
fc.property(fc.set(TrackNameArb, 1, 10), CommandsArb, (initialTracks, commands) => {
const real = new MusicPlayer(initialTracks);
const model = new MusicPlayerModel();
model.numTracks = initialTracks.length;
for (const t of initialTracks) {
model.tracksAlreadySeen[t] = true;
}
CommandExecutor(() => ({ model, real }), commands);
})
));
});
5 changes: 3 additions & 2 deletions example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@
"test:contains": "mocha contains/test.js",
"test:knight": "mocha shadows-of-the-knight-codingame/test.js",
"test:settings": "mocha optional-settings-combination/test.js",
"test": "npm run test:contains && npm run test:knight"
"test:model": "mocha --require ts-node/register model-based-testing/test.ts"
},
"devDependencies": {
"fast-check": "file:..",
"mocha": "^5.0.0"
"mocha": "^5.0.0",
"ts-node": "^6.1.0"
},
"author": "Nicolas DUBIEN <github@dubien.org>",
"license": "MIT"
Expand Down
2 changes: 1 addition & 1 deletion example/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ npm install

status=0

for testUnit in "contains:6:0" "knight:15:1" "settings:0:1"
for testUnit in "contains:6:0" "knight:15:1" "settings:0:1" "model:1:0"
do
name=`echo "${testUnit}" | cut -d: -f1`
success=`echo "${testUnit}" | cut -d: -f2`
Expand Down

0 comments on commit b80b4f9

Please sign in to comment.