Creating a Stub Instance Using createStubInstance: Constructor Not Skipped as Expected

27 Views Asked by At

I have the following problem:

I have a class, which is a State controller and I want to test it when its in the "start" state. In particular its update method, which switches to the next state when updated with particular events

export class StateController implements IOBserver{
    private logger: LoggerService;

    private stateConfiguration: StateConfiguration | undefined;

    private currStateNum: number;

    private maxStateNum: number;

    private controllerName: string;

    constructor(stateName: string) {
        this.controllerName = StateController.name;
        this.logger = ServiceLocator.Current.get<LoggerService>(LoggerService.name);
        this.stateConfiguration = new StateData().getSelectedStateConfiguration(stateName);
        this.currStateNum = 0;
        this.maxStateNum = 0;
    }

    public async startStateMachine() {
        // .. starting the state machine
    }

    public async update(event: EventType): Promise<void> {
        if (event === EventType.Finish) {
            await this.transitionToNextState();
        } else {
            if (event === EventType.UnknownError || event === EventType.UnresolvableError) {
                await new CleanUpState().onStart();
                process.exit(1);
            } else {
                await new RecoveryState(event).onStart();
            }
        }
    }

    private async goToNextState(): Promise<void> {
      // ...
    }

In the constructor you can see we have the stateConfiguration, which looks like that:

export class StateData {

    public getSelectedStateConfiguration(stateName: string): StateConfiguration | undefined {
        switch (stateName) {
            case 'start':
                return this.getStartConfiguration();
            // Other state cases
        }
    }

    private getStartConfiguration(): StateConfiguration {
        return {
            'stateMachineName' : 'start',
            'states' : [
                new InitState(),
                new StartState(),
                new NetworkPrepState(),
                new AccountCreationState(),
                new CleanUpState(),
                new AttachState()
                
            ]
        };
    }
}

I have written the following two tests, however the second one does not work, because it tries to go through the constructor of the CleanUpState, although I am creating a stubInstance and I would expect it to not go through the constructor of the CleanUpState

The test file:

// .. imports 

describe('StateController', () => {
  let stateController: StateController;
  let serviceLocatorStub: sinon.SinonStub;
  let cleanUpStateStub: sinon.SinonStubbedInstance<CleanUpState>;
  let cleanUpStateStub2 : sinon.SinonStubbedInstance<CleanUpState>;
  let getStartConfigurationStub: sinon.SinonStub;

  beforeEach(() => {
    // Create a stub for the ServiceLocator
    const stubbedInitState = sinon.createStubInstance(InitState);
    const stubbedStartState = sinon.createStubInstance(StartState);
    const stubbedNetworkPrepState = sinon.createStubInstance(NetworkPrepState);
    const stubbedAccountCreationState = sinon.createStubInstance(AccountCreationState);
    cleanUpStateStub = sinon.createStubInstance(CleanUpState);
    cleanUpStateStub.onStart.resolves();
    const stubbedAttachState = sinon.createStubInstance(AttachState);
    const loggerServiceStub = sinon.createStubInstance(LoggerService);

    serviceLocatorStub = sinon.stub(ServiceLocator.Current, 'get').returns(loggerServiceStub);

    getStartConfigurationStub = sinon.stub(StateData.prototype, <any>"getStartConfiguration").returns({
        'stateMachineName' : 'start',
        'states' : [
          stubbedInitState,
          stubbedStartState,
          stubbedNetworkPrepState,
          stubbedAccountCreationState,
          cleanUpStateStub,
          stubbedAttachState
        ]
    });
    stateController = new StateController('start');
  });

  afterEach(() => {
    // Restore the original ServiceLocator after each test
    cleanUpStateStub.onStart.restore();
    serviceLocatorStub.restore();
    getStartConfigurationStub.restore();
  });

  it('should transition to next state on Finish event', async () => {
    // Starting the state machine
    await stateController.startStateMachine();

    // Sending two finish events
    await stateController.update(EventType.Finish);
    await stateController.update(EventType.Finish);

    expect(stateController['currStateNum']).to.equal(2);
  });


  it('should handle other events correctly', async () => {
    // Arrange
    // Stub process.exit
    const processExitStub = sinon.stub(process, 'exit');
    // Act
    // await stateController.update(EventType.UnknownError);
    await stateController.update(EventType.UnresolvableError);

    // Assert
    assert.isTrue(cleanUpStateStub2.onStart.calledOnce);
    assert.isTrue(processExitStub.calledWith(1));
  });
});

As you can see I use a stubInstance and stub the onStart but it is not working.

The way the CleanUpState is called in the else in the update method is similar to the way StateData is used in the constructor. I stubbed the getSelectedStateConfiguration and I tried the same way for the CleanUpState but its also not working

getStartConfigurationStub = sinon.stub(CleanUpState.prototype, <any>"onStart").resolved();

I have read this Can ES6 constructors be stubbed more easily with Sinon? but I still do not quite get why this wont work, but it works for StateData

0

There are 0 best solutions below