olejniczag
olejniczag

Reputation: 394

How to write a unit test for typed class?

I've been learning to write unit tests of JavaScript/TypeScript code using Jest library. Here's an example I don't know how to approach to. It's typed with TypeScript - there's only two public methods and a constructor which require service1 argument.

I think I need to test two situations:

My problems are:

export class Class4 {
    private attr: number;
    private intervalId;

    constructor(private service1) { }

    public method() {
        this.intervalId = setInterval(() => {
            if (this.service1.get() > 42) {
                this.end()
            } else {
                this.attr++;
            }
        }, 100);
    }

    public getAttr() {
        return this.attr;
    }

    private end() {
        clearInterval(this.intervalId);
    }
}

I need your help in writing test in Jest only for 2 situations I described.

Edit. Here's simple test based on this class. It's not assigning a value of this.attr (my argument's value gets assinged to service1 though) and after running a test I receive an error message

Expected: 40 Received: undefined

Code:

    it('should stop incrementing Class4.attr if it\'s > 42', () => {
        const class4 = new Class4(40);
        const attrVal = class4.getAttr();
        expect(attrVal).toBe(40);
    });

Upvotes: 2

Views: 1447

Answers (1)

remix23
remix23

Reputation: 3019

I'm not very sure this can help but below is an example of how you could use Jest to test something like this.

It's your code translated from typescript to es6 with a light fake Jest implementation attached. It's in a separate script to leave the example itself alone.

The fake Jest only implements the required Jest matchers in this test: expect, toBeGreaterThan, not, toHaveBeenCalledTimes.

And the following Jest utilities: useFakeTimers, advanceTimersByTime, clearAllTimers, mock

// self calling function is required to simulate Class4 module and for fake Jest mock to work
(function() {
// translated from typescript to es6
class Class4 {
    attr = 0;

    intervalId = null;

    constructor(service1) {
        this.service1 = service1;
    }

    method() {
        this.intervalId = setInterval(() => {
            if (this.service1.get() > 42) {
                this.end();
            } else {
                this.attr++;
            }
        }, 100);
    }

    getAttr() {
        return this.attr;
    }

    end() {
        clearInterval(this.intervalId);
    }
}
// this is required to simulate Class4 module and for fake Jest mock to work
window.Class4 = Class4;
})();

// even if we do not know exactly what Service is,
// we know that it has a get method which returns a varying number.
// so this implementation of Service will do
// (it's ok since we're testing Class4, not Service)
class ServiceImpl {
    v = 0;
    set(v) { this.v = v; }
    get() { return this.v; }
}

// after this call, jest will control the flow of
// time in the following tests
// (reimplements the global methods setInterval, setTimeout...etc)
jest.useFakeTimers();

// actually it should be jest.mock('<path to your module>')
// but remember we're using a fake Jest working in SO's snippet)
// now Class4 is a mock
jest.mock(Class4);

// we need a Service instance for a Class4 object to be instanciated
const service = new ServiceImpl();

const class4 = new Class4(service);

it('Class4 constructor has been called 1 time', () => {
    expect(Class4).toHaveBeenCalledTimes(1);
});

it('should be incrementing Class4.attr if service.get() < 42', () => {
    // service.get() will return 40
    service.set(40);

    // storing initial attr val
    let lastAttrVal = class4.getAttr();

    // now class4 is running and should be incrementing
    class4.method();

    // jest controls the time, advances time by 1 second
    jest.advanceTimersByTime(1000);

    expect(class4.getAttr()).toBeGreaterThan(lastAttrVal);
});

it('should have been called Class4.end 0 time', () => {
    expect(Class4.mock.instances[0].end).toHaveBeenCalledTimes(0);
});

it('should stop incrementing Class4.attr if service.get() > 42', () => {
    // service.get() will now return 45, this should end class4
    // incrementation in the next interval
    service.set(45);

    // storing current attr val
    let lastAttrVal = class4.getAttr();

    jest.advanceTimersByTime(1000);

    expect(class4.getAttr()).not.toBeGreaterThan(lastAttrVal);

});

it('end should have been called end 1 time', () => {
    expect(Class4.mock.instances[0].end).toHaveBeenCalledTimes(1);
});

jest.clearAllTimers();
<script type="text/javascript">
window.jest = {};
jest.useFakeTimers = () => {
    jest.oldSetTimeout = window.setTimeout;
    jest.oldSetInterval = window.setInterval;
    jest.oldClearTimeout = window.clearTimeout;
    jest.oldClearInterval = window.clearInterval;
    jest.time = 0;
    jest.runningIntervals = [];
    window.setInterval = (callback, delay) => {
        let interIndex = jest.runningIntervals.findIndex(i => i.cancelled);
        let inter = interIndex !== -1 && jest.runningIntervals[interIndex];
        if (!inter) {
            inter = {};
            interIndex = jest.runningIntervals.length;
            jest.runningIntervals.push(inter);
        }
        Object.assign(
            inter,
            {
                start: jest.time,
                last: jest.time,
                callback,
                delay,
                cancelled: false
            }
        );
        callback();
        return interIndex;
    };
    window.clearInterval = idx => {
        jest.runningIntervals[idx].cancelled = true;
    };
    jest.advanceTimersByTime = advance => {
        for (const end = jest.time + advance;jest.time < end; jest.time++) {
            jest.runningIntervals.forEach(inter => {
                if (!inter.cancelled && jest.time - inter.last >= inter.delay) {
                    inter.last = jest.time;
                    inter.callback();
                }
            });
        }
    };
    jest.clearAllTimers = () => {
        jest.runningIntervals.length = 0;
        window.setTimeout = jest.oldSetTimeout;
        window.setInterval = jest.oldSetInterval;
        window.clearTimeout = jest.oldClearTimeout;
        window.clearInterval = jest.oldClearInterval;
    };
};

jest.resolve = (v) => {
  console.log(v ? 'PASS' : 'FAIL');
}
window.it = (description, test) => {
    console.log(description);
    test();
};
window.expect = (received) => {
  return {
    toBeGreaterThan: (expected) => jest.resolve(received > expected),
    not: {
      toBeGreaterThan: (expected) => jest.resolve(received <= expected),
    },
    toHaveBeenCalledTimes: (expected) => jest.resolve((received ? received.mock.calls.length : 0) === expected),
  }
}
jest.mock = (cls) => {
    if (cls.mock) return;
    const mock = {
        instances: [],
        calls: []
    }
    const proto0 = cls.prototype;

    function ClassMock(...args) {
        mock.calls.push(args);
        
        this.instance = new proto0.constructor(...args);
        this.instanceMock = {};
        mock.instances.push(this.instanceMock);
        Object.getOwnPropertyNames(proto0).forEach((member) => {
          if (member === 'constructor' || typeof proto0[member] !== 'function') return;
          this.instanceMock[member] = this.instanceMock[member] || { mock: { calls: [] } };
          this.instance[member] = (function(...args) {
              this.instanceMock[member].mock.calls.push(args);
              return proto0[member].apply(this.instance, [args]);
          }).bind(this);
      });
    }

    Object.getOwnPropertyNames(proto0).forEach((member) => {
        if (member === 'constructor' || typeof proto0[member] !== 'function') return;
        ClassMock.prototype[member] = function(...args) {
            return this.instance[member](...args);
        }
    });
    
    
    ClassMock.mock = mock;
    window[proto0.constructor.name] = ClassMock;
}
</script>

Upvotes: 1

Related Questions