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
Thanks again to @mark-seemann for his socratic questions. Now that I have finish this mini-project, I can reflect on it, and I know can to the conclusion (I reserve the right to change my mind though), that those non-deterministic providers aren't entities props, so I now prefer to inject them trough the public method params when necessary.
I also prefer to pass the whole dependency in the param than the result of its call as it gives better flexibility.