I have a class method I want to test:
setStepResolution(resolution: stepResolution): void {
switch (resolution) {
case stepResolution.FULL_SETUP:
this.stepperMotors.left.ms1Pin.digitalWrite(0)
this.stepperMotors.left.ms2Pin.digitalWrite(0)
this.stepperMotors.left.ms3Pin.digitalWrite(1)
this.stepperMotors.right.ms1Pin.digitalWrite(0)
this.stepperMotors.right.ms2Pin.digitalWrite(0)
this.stepperMotors.right.ms3Pin.digitalWrite(1)
break
case stepResolution.HALF_STEP:
this.stepperMotors.left.ms1Pin.digitalWrite(1)
this.stepperMotors.left.ms2Pin.digitalWrite(0)
this.stepperMotors.left.ms3Pin.digitalWrite(0)
this.stepperMotors.right.ms1Pin.digitalWrite(1)
this.stepperMotors.right.ms2Pin.digitalWrite(0)
this.stepperMotors.right.ms3Pin.digitalWrite(0)
break
Each of these digitalWrite
calls is made to an instance of a different class that are created when my class is constructed:
export default class BotController {
private stepperMotors: StepperMotorCollection
constructor() {
this.initalizeMotors()
}
private initalizeMotors(): void {
this.stepperMotors = {
left: {
directionPin: new Gpio(Number(process.env.LEFT_DIRECTION_PIN), { mode: Gpio.OUTPUT }),
stepPin: new Gpio(Number(process.env.LEFT_STEP_PIN), { mode: Gpio.OUTPUT }),
ms1Pin: new Gpio(Number(process.env.LEFT_RESOLUTION_PIN_MS1), { mode: Gpio.OUTPUT }),
ms2Pin: new Gpio(Number(process.env.LEFT_RESOLUTION_PIN_MS2), { mode: Gpio.OUTPUT }),
ms3Pin: new Gpio(Number(process.env.LEFT_RESOLUTION_PIN_MS3), { mode: Gpio.OUTPUT }),
stepsPerMM: Number(process.env.LEFT_STEPS_PER_MM),
swapCoils: Boolean(process.env.LEFT_SWAP_COILS),
},
right: {
directionPin: new Gpio(Number(process.env.RIGHT_DIRECTION_PIN), { mode: Gpio.OUTPUT }),
stepPin: new Gpio(Number(process.env.RIGHT_STEP_PIN), { mode: Gpio.OUTPUT }),
ms1Pin: new Gpio(Number(process.env.RIGHT_RESOLUTION_PIN_MS1), { mode: Gpio.OUTPUT }),
ms2Pin: new Gpio(Number(process.env.RIGHT_RESOLUTION_PIN_MS2), { mode: Gpio.OUTPUT }),
ms3Pin: new Gpio(Number(process.env.RIGHT_RESOLUTION_PIN_MS3), { mode: Gpio.OUTPUT }),
stepsPerMM: Number(process.env.RIGHT_STEPS_PER_MM),
swapCoils: Boolean(process.env.RIGHT_SWAP_COILS),
},
}
}
I could create a mock for the stepperMotors
property in my test with mocks of the Gpio
class (I'm already mocking the constructor for some of the other tests):
test("can change step resolution", () => {
// * The step resolution of the stepper motors can be changed via the code.
// * The settings can be controlled by an enum that denotes each of the possible
// * resolutions.
const mockStepperMotorConfiguration: StepperMotorCollection = {
left: {
directionPin: new pigpio.Gpio(1),
stepPin: new pigpio.Gpio(1),
ms1Pin: new pigpio.Gpio(1),
ms2Pin: new pigpio.Gpio(1),
ms3Pin: new pigpio.Gpio(1),
stepsPerMM: 1,
swapCoils: false,
},
right: {
directionPin: new pigpio.Gpio(1),
stepPin: new pigpio.Gpio(1),
ms1Pin: new pigpio.Gpio(1),
ms2Pin: new pigpio.Gpio(1),
ms3Pin: new pigpio.Gpio(1),
stepsPerMM: 1,
swapCoils: false,
},
}
// ^ To change the resolution to a full step
// * send in the full step enum
newController.setStepResolution(stepResolution.FULL_SETUP)
But I can't because the stepperMotor
property is private.
There are several ways I could solve this (make the property public, make a public method for setting the property), but neither of them seems desirable because the property should never be accessible outside of the class so I would be exposing properties or methods only to support testing.
Is there another way of doing this kind of test? I know in jest I can mock a method on a class in javascript by replacing the prototype function e.g.:
BotController.prototype.someMethod = jest.fn()
const controller = new BotController
And if this was a class I was trying to mock I could pass in the properties as a mock implementation, e.g.:
jest.mock("../BotController", () => ({
stepperMotors: mockStepperMotorConfiguration
}))
But then everything else in the class would also be mocked and you'd loose the point.
Any idea on how I should approach this?
I'm trying out Taplar's approach of creating a backdoor.
I tried casting my controller instance as an any
:
But the complier is still yelling at me:
After Taplar pointed out how to call the method on the cast version the errors went away on the back door which is fantastic!
The next wall I smashed into was that now for some reason the test can't see my mock anymore which is weird because the variable is local to the test.
Unless #
hard privacy is used, private properties can be accessed outside a class at runtime, TypeScript access modifiers are applied only at compilation time.
Accessing private members in tests can be considered a reflection.
Visibility can be bypassed with bracket notation, which is the preferable option:
controllerInstance['stepperMotors'] = ...;
Or with Reflect API:
Reflect.set(controllerInstance, 'stepperMotors', ...);
Or by disabling type checks:
(controllerInstance as any).stepperMotors = ...;
Since private property is set with prototype method, another approach is to mock it. It's applicable if original initialization causes undesirable side effects and needs to be avoided. BotController.prototype.someMethod = jest.fn()
should never be used in Jest as it cannot be automatically cleaned up and cross-contaminates tests. Instead it could be:
jest.spyOn(BotController.prototype, 'initalizeMotors').mockImplementation(function (this: BotController) {
this['stepperMotors'] = ...;
});
...
expect(controllerInstance['initalizeMotors']).toHaveBeenCalled();
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With