I am working on a frontend repository that implements an hexagonal architecture with domain driven design, using Redux Toolkit.
It is being developed in a TDD fashion. For that purpose, I am using "hand made" mocks. That way, I can pass a real implementation in the SUT, but an InMemory implementation in the test suites.
Examples (you can access the repository here: https://github.com/amehmeto/HexaTenzies):
rollDice.spec.ts
import { ReduxStore } from '../../../../react-view/main'
import { configureStoreWith } from '../../../../app/store'
import { InMemoryIdProvider } from '../../../../infrastructure/idProvider/InMemoryIdProvider'
import { InMemoryRandomNumberProvider } from '../../../../infrastructure/randomNumberProvider/InMemoryRandomNumberProvider'
import { Die } from '../../entities/Die'
import { IdProvider } from '../../ports/IdProvider'
import { rollDice } from './rollDice'
import { Dice } from '../../entities/Dice'
function dieDataBuilder() {
return new Die('uuid', {
value: 2,
isHeld: false,
})
}
async function triggerRollDiceUseCase(store: ReduxStore) {
await store.dispatch(rollDice())
return store.getState().dice.dice
}
describe('Generate Random Dice', () => {
let store: ReduxStore
let idProvider: IdProvider
let randomNumberProvider: InMemoryRandomNumberProvider
beforeEach(() => {
idProvider = new InMemoryIdProvider()
randomNumberProvider = new InMemoryRandomNumberProvider()
const dependencies = {
idProvider: idProvider,
randomNumberProvider: randomNumberProvider,
}
store = configureStoreWith(dependencies)
})
it('should generate new dice after every roll', async () => {
const expectedNumberOfDie = 10
const firstDice = await triggerRollDiceUseCase(store)
randomNumberProvider.with(0.5)
const secondDice = await triggerRollDiceUseCase(store)
expect(firstDice.length).toBe(expectedNumberOfDie)
expect(secondDice.length).toBe(expectedNumberOfDie)
expect(firstDice).not.toStrictEqual(secondDice)
})
The contract
randomNumberProvider.ts
export interface RandomNumberProvider {
generate(): number
}
The in memory implementation:
InMemoryRandomNumberProvier.ts
import { RandomNumberProvider } from '../../core/dice/ports/randomNumberProvider'
export class InMemoryRandomNumberProvider implements RandomNumberProvider {
// Should be greater or equal to 0 and less than 1 to simulate Math.random()
private controlledRandomNumber = 0.3
generate(): number {
return this.controlledRandomNumber
}
with(number: number): void {
this.controlledRandomNumber = number
}
}
The real implementation :
RealRandomNumberProvider.ts
import { RandomNumberProvider } from '../../core/dice/ports/randomNumberProvider'
export class RealRandomNumberProvider implements RandomNumberProvider {
generate(): number {
return Math.random()
}
}
That way, I have control over the non deterministic value on my test. I retrieved those providers in the thunk like so:
import { createAsyncThunk } from '@reduxjs/toolkit'
import { DieViewModel } from '../../entities/Die'
import { Dice } from '../../entities/Dice'
import { ExtraDependencies } from '../../extraDependencies'
export const rollDice = createAsyncThunk<
DieViewModel[],
void,
ExtraDependencies
>(
`dice/rollDice`,
async (thunkAPI, { extra: { randomNumberProvider, idProvider } }) => {
return new Dice(randomNumberProvider, idProvider).roll()
},
)
What bother me is this line:
return new Dice(randomNumberProvider, idProvider).roll()
I couldn't find a way to design the aggregate root Dice without injecting it those provider, in order to provider an id and a random number to its child entities Die.
Dice.ts
import { RandomNumberProvider } from '../ports/randomNumberProvider'
import { IdProvider } from '../ports/IdProvider'
import { Die, DieViewModel } from './Die'
export class Dice {
private readonly AMOUNT_OF_DICE = 10
private readonly dice: Die[]
constructor(
private randomNumberProvider: RandomNumberProvider,
private idProvider: IdProvider,
) {
this.dice = this.initializeDice()
}
roll(): DieViewModel[] {
return this.dice.map((die) => {
const randomNumber = this.randomNumberProvider.generate()
die.roll(randomNumber)
return die.toViewModel()
})
}
public initializeDice(): Die[] {
return Array(this.AMOUNT_OF_DICE)
.fill(undefined) // needed to avoid generating die with the same id
.map(() => this.generateDie())
}
private generateDie() {
const newId = this.idProvider.getNew()
return new Die(newId)
}
}
Die.ts
export interface DieViewModel {
id: string
props: DieProps
}
interface DieProps {
value: number
isHeld: boolean
}
export class Die {
private readonly MIN_VALUE = 1
private readonly MAX_VALUE = 6
constructor(
public readonly id: string,
readonly props: DieProps = {
value: 6,
isHeld: false,
},
) {
this.props = props
}
public roll(randomNumber: number): void {
this.props.value = ~~(randomNumber * this.MAX_VALUE) + this.MIN_VALUE
}
public hold(): void {
this.props.isHeld = !this.props.isHeld
}
static fromViewModel(dieViewModel: DieViewModel): Die {
const { id, props } = dieViewModel
return new Die(id, props)
}
toViewModel(): DieViewModel {
return {
id: this.id,
props: {
value: this.props.value,
isHeld: this.props.isHeld,
},
}
}
}
I am also concerned but the method roll(randomNumber) of Die which I guess leak some logic (random number) that should be encapsulated.
How can I redesign those Aggregate Root and Entity?
Again, you can access the repository code here: (you can access the repository here: https://github.com/amehmeto/HexaTenzies
idProviderand arandomNumberProviderprop to myDiceaggregate is wrong, as real dice doesn't have such features. Those providers feel more like external technicalities that should live in the usecase. Typically, before trying to mock those non-determistic providers, I would have just calleduuid()andMath.random()directly with DI.