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 = elements(sampleAccounts); const genBalance: Gen = choose(0, 1000000); const genAccounts: Gen> = nonEmptyMap( genAccount, genBalance, ); const genGetBalance: (accounts: Map) => Gen = (accounts: Map) => (rng: Prando) => (size: number) => { const account = elements(Array.from(accounts.keys()))(rng)(size); return {op: 'GetBalance', account}; }; const genDeposit: (accounts: Map) => Gen = (accounts: Map) => (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) => Gen = (accounts: Map) => (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) => frequency([ [1, genGetBalance(accounts)], [3, genDeposit(accounts)], [3, genWithdraw(accounts)], ]); const genOperations = (accounts: Map) => listOf(genOneOperation(accounts)); const correctBalance = (initialAccounts: Map) => (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'); }); });