dvabuzyarov / moq.ts

Moq for Typescript
Apache License 2.0
120 stars 9 forks source link

How does `mimics` work? #993

Closed runia1 closed 1 year ago

runia1 commented 1 year ago

When I read the docs on mimics I originally thought that it would allow me to create a mock which would proxy some method calls to the origin, and others could be intercepted. For example if you had the below code

import { Mock, Times } from 'moq.ts';

class A {
    public b() {
        console.log('Real b called');

        // here method "b" calls method "c" which we'd like to intercept with our mock
        // rather than calling the real "c" below.
        this.c();
    }

    public c() {
        console.log('Real c called');
    }
}

const a = new A();

const aMimic = new Mock<A>()
    // mock the call to method "c"
    .setup((i) => i.c())
    .returns(void 0)
    // allow the call to "b" to invoke the real method
    .setup((i) => i.b())
    .mimics(a);

const wrappedA = aMimic.object();

// invoke method "b"
wrappedA.b();

// verify that the call to the real method "b" resulted in a call to
// our mock of method "c".
aMimic.verify((i) => i.c(), Times.Once());

This doesn't work. The output I get is

$ npx ts-node test.ts
Real b called
Real c called
c() should be called once, but was called 0 time(s)
-------------------------------------
Tracked calls:
Getter of 'b'
b()
-------------------------------------

I would expect the output to be

$ npx ts-node test.ts
Real b called

Obviously I don't understand how to use mimics so I'm curious what the use case for mimics would be?

dvabuzyarov commented 1 year ago

Mimics just proxies the call to the object. The object is not patched or some how else altered, so when you call method "c" you just call it. The only way to intercept it through mock object is to invoke it on the mock.

wrappedA.с();

The whole point of mimics is to proxy interactions to another object. The use cases are different, in my project I am using it to reproduce a complicated behaviour.

    resolveMock(Actions)
      .setup(It.IsAny())
      .mimics(cold("a", {a: action}));

Actions is an observable in the production code but in the unit test context it is replaced with a mock. And the mock is proxies interactions to a special test friendly observable.

Another examples:

    resolveMock(EnterZoneScheduler)
      .setup(It.IsAny())
      .mimics(getTestScheduler() as any);
    resolveMock(Subscription)
      .setup(It.IsAny())
      .mimics(new Subscription());

Plus I would re-factore your code and move method "c" into a separated unit.

describe("#993 How does mimics work? ", () => {

    class A {
        constructor(private readonly dep: A) {
        }
        public b() {
            console.log('Real b called');

            // here method "b" calls method "c" which we'd like to intercept with our mock
            // rather than calling the real "c" below.
            this.dep.c();
        }

        public c() {
            console.log('Real c called');
        }
    }

    it("the issue", async () => {
        const mock = new Mock<A>();
        const a = new A(mock.object());

        const aMimic = mock
            // mock the call to method "c"
            .setup((i) => i.c())
            .returns(void 0)
            // allow the call to "b" to invoke the real method
            .setup((i) => i.b())
            .mimics(a);

        const wrappedA = aMimic.object();

        // invoke method "b"
        wrappedA.b();

        // verify that the call to the real method "b" resulted in a call to
        // our mock of method "c".
        aMimic.verify((i) => i.c(), Times.Once());
    });
});
runia1 commented 1 year ago

Thanks for getting back to me so quickly!

If I'm understanding this correctly, the main use case for mimics is to wire up fakes for DI tokens, and the moq.ts functionality mainly provides a way to spy on those fakes. Is that accurate?

dvabuzyarov commented 1 year ago

Yes, you are right. This was exactly the case that led to the mimics implementation.