1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
|
import {toNullable} from 'fp-ts/lib/Option';
import {Account, Operation, SimpleBank} from './bank';
import {
Gen,
Property,
choose,
elements,
forall,
frequency,
keyValueOf,
listOf,
mapOf1 as nonEmptyMap,
property,
} from './property';
import Prando from 'prando';
const sampleAccounts: Account[] = [
'FR12345678901234',
'DE2345678901234',
'IT34567890123412',
];
const genAccount: Gen<Account> = elements(sampleAccounts);
const genBalance: Gen<number> = choose(0, 1000000);
const genAccounts: Gen<Map<Account, number>> = nonEmptyMap(
genAccount,
genBalance,
);
const genGetBalance: (accounts: Map<Account, number>) => Gen<Operation> =
(accounts: Map<Account, number>) => (rng: Prando) => (size: number) => {
const account = elements(Array.from(accounts.keys()))(rng)(size);
return {op: 'GetBalance', account};
};
const genDeposit: (accounts: Map<Account, number>) => Gen<Operation> =
(accounts: Map<Account, number>) => (rng: Prando) => (size: number) => {
const account = elements(Array.from(accounts.keys()))(rng)(size);
const gAmt = choose(1, 100000)(rng)(size);
return {op: 'Deposit', account, amount: gAmt};
};
const genWithdraw: (accounts: Map<Account, number>) => Gen<Operation> =
(accounts: Map<Account, number>) => (rng: Prando) => (size: number) => {
const account = elements(Array.from(accounts.keys()))(rng)(size);
const gAmt = choose(1, 100000)(rng)(size);
return {op: 'Withdraw', account, amount: gAmt};
};
const genOneOperation = (accounts: Map<Account, number>) =>
frequency([
[1, genGetBalance(accounts)],
[3, genDeposit(accounts)],
[3, genWithdraw(accounts)],
]);
const genOperations = (accounts: Map<Account, number>) =>
listOf(genOneOperation(accounts));
const correctBalance =
(initialAccounts: Map<Account, number>) =>
(ops: Operation[]): boolean => {
const expectedBalances = new Map(initialAccounts);
const api = new SimpleBank(initialAccounts);
for (const op of ops) {
switch (op.op) {
case 'GetBalance': {
const retrieved = toNullable(api.balance(op.account));
const expected = expectedBalances.get(op.account);
if (retrieved !== expected) {
console.log(
`Balance mismatch for account ${op.account}: expected ${expected}, got ${retrieved}`,
);
return false;
}
break;
}
case 'Deposit': {
api.deposit(op.account, op.amount);
const current = expectedBalances.get(op.account) || 0;
expectedBalances.set(op.account, current + op.amount);
break;
}
case 'Withdraw': {
api.withdraw(op.account, op.amount);
const current = expectedBalances.get(op.account) || 0;
expectedBalances.set(op.account, current - op.amount);
break;
}
}
}
// finally check all balances
for (const [account, expectedBal] of expectedBalances.entries()) {
const retrieved = toNullable(api.balance(account));
if (retrieved !== expectedBal) {
console.log(
`Final balance mismatch for account ${account}: expected ${expectedBal}, got ${retrieved}`,
);
return false;
}
}
return true;
};
describe('Bank Simulator', () => {
test('can retrieve balance given account exists', () => {
const prop = forall(genAccounts, accounts => {
const api = new SimpleBank(accounts);
return property(keyValueOf(accounts), ([accountNr, bal]) => {
const retrieved = toNullable(api.balance(accountNr));
return retrieved == bal;
});
});
const result = Property.run(prop);
expect(result.result).toBe('OK');
});
test('correctly update balance given a sequence of deposits and withdrawals', () => {
const prop = forall(genAccounts, accounts => {
return property(genOperations(accounts), correctBalance(accounts));
});
const result = Property.run(prop);
expect(result.result).toBe('OK');
});
});
|