How can I mock a private property in a class I'm trying to test in jest

Viewed 14003

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?

Update: trying to create a backdoor

I'm trying out Taplar's approach of creating a backdoor.

I tried casting my controller instance as an any:

making a backdoor

But the complier is still yelling at me:

blergh

Another update

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.

closer

1 Answers

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();
Related