Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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

like image 293
Chris Schmitz Avatar asked Dec 17 '22 12:12

Chris Schmitz


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();
like image 67
Estus Flask Avatar answered Feb 15 '23 10:02

Estus Flask