Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unable to use mocks with angular TestBed #27

Closed
Styrna opened this issue Mar 11, 2019 · 15 comments · Fixed by #281
Closed

Unable to use mocks with angular TestBed #27

Styrna opened this issue Mar 11, 2019 · 15 comments · Fixed by #281
Milestone

Comments

@Styrna
Copy link

Styrna commented Mar 11, 2019

Love the simplicity of lib but im unable to use it in component testing with angular TestBed.

Test:

describe('FilterContentWidgetComponent', () => {
    let component: FilterContentWidgetComponent;
    let fixture: ComponentFixture<FilterContentWidgetComponent>;

    let filterMapperMock = Substitute.for<FilterMapper>();
    
    beforeEach(async() => {
        TestBed.configureTestingModule( {
            declarations: [ 
                FilterContentWidgetComponent,
            ],
            imports: [ HttpModule], 
            providers: [ 
                { provide: FilterMapper, useValue: <FilterMapper>filterMapperMock},
                
             ]
        }).compileComponents();
    });

    beforeEach(() => {
        fixture = TestBed.createComponent(FilterContentWidgetComponent);
        component = fixture.componentInstance;
      });

    it('should create', () => {
        fixture.detectChanges();

        expect(component).toBeTruthy();
    });
});

Not sure if am doing sth wrong but i get following exception.

TypeError: Cannot convert object to primitive value at JitEmitterVisitor._emitReferenceToExternal (http://localhost:9876/node_modules/@angular/compiler/fesm2015/compiler.js?:4693:44) at JitEmitterVisitor.visitExternalExpr (http://localhost:9876/node_modules/@angular/compiler/fesm2015/compiler.js?:4662:1) at ExternalExpr.visitExpression (http://localhost:9876/node_modules/@angular/compiler/fesm2015/compiler.js?:1293:1) at visitAllObjects.expr (http://localhost:9876/node_modules/@angular/compiler/fesm2015/compiler.js?:4387:1) at JitEmitterVisitor.visitAllObjects (http://localhost:9876/node_modules/@angular/compiler/fesm2015/compiler.js?:4406:1) at JitEmitterVisitor.visitAllExpressions (http://localhost:9876/node_modules/@angular/compiler/fesm2015/compiler.js?:4387:1) at JitEmitterVisitor.visitInvokeFunctionExpr (http://localhost:9876/node_modules/@angular/compiler/fesm2015/compiler.js?:4218:1) at JitEmitterVisitor.visitInvokeFunctionExpr (http://localhost:9876/node_modules/@angular/compiler/fesm2015/compiler.js?:4547:1) at InvokeFunctionExpr.visitExpression (http://localhost:9876/node_modules/@angular/compiler/fesm2015/compiler.js?:1250:1) at visitAllObjects.expr (http://localhost:9876/node_modules/@angular/compiler/fesm2015/compiler.js?:4387:1)

@ffMathy
Copy link
Owner

ffMathy commented Mar 11, 2019

Can you make an isolated example and upload it as a zip file?

@Styrna
Copy link
Author

Styrna commented Mar 12, 2019

Sure here is isolated example

Its ng new with 3 files:
foo-component.ts
foo-service.ts
foo-component.spec.ts

Sample.zip

@domasx2
Copy link
Contributor

domasx2 commented Mar 23, 2019

I encountered a similar problem.
I think the issue is that if no return value is set up for a property/method on the mock, it returns a Proxy object. But node's util.inspect expects a primitive value in some cases. See nodejs/node#25212

Declaring return values for each method/property on the mock that is used by the test should solve it.

I think this is kinda an architectural issue with this lib.
If it was to set up return values via a special accessor, smth like substitute.setup.foo(1, 2).returns(3) instead of substitute.foo(1, 2).returns(3), and methods/properties with no return values defined were to return a primitive value or throw, this issue could be avoided

@ffMathy
Copy link
Owner

ffMathy commented Mar 26, 2019

@domasx2 not a bad idea.

What about calling .build() on the final object instead?

var fake = Substitute.for<Foo>();
fake.foo(1, 2).returns(3);

fake.build();

Or something similar? .finalize() perhaps?

@Styrna
Copy link
Author

Styrna commented Mar 26, 2019

yeah but that would require you to build a fake before you initailize testbed

i usually start mocking functions in tests (as those are case specyfic) not in beforeeach statments.

@ffMathy
Copy link
Owner

ffMathy commented Mar 26, 2019

Why would it require you to build them before?

@Styrna
Copy link
Author

Styrna commented Mar 26, 2019

The exception from JitEmitterVisitor happens during testbed configureTestingModule with mostly done in beforeeach statments of the test.spec.

@mike-hanson
Copy link
Contributor

@Styrna

When I switched from AngularJS to Angular on release of v2 I tried to use my own library jsSubstitute for mocking services and the conclusion I came to was that it just isn't necessary and using anything other than the default Jasmine spies isn't worth the grief and effort when testing Components.

Don't get me wrong I think substitute.js is a great improvement on the other libraries targeting TypeScript (including my own) but for testing Angular Components I don't think a library is the way to go. When testing Angular services, yes libraries can be helpful since these are typically just tests that exercise the Class.

After trying several solutions I settled for simply providing services in the test module then using TestBed.get(MyService) to get an instance in the test via DI and using Jasmine spyOn to mock just the part/s of the service I am going to touch during the particular test.

I know this doesn't provide a solution, but I would prefer that this library stay true to the fluent and readable NSubstitute pattern rather than going down the route of having to use an .Object property or calling a method to pre-build a substitute.

@Styrna
Copy link
Author

Styrna commented Mar 27, 2019

I would argue here.

Spies are not mocks and they break isolation of unit tests. Components should be tested the same way as services are. This lib was compared with ts-mockito and i would favored it over ts-mockito as its more simple (we use nsubstitute on backend testing). But since im unable to test components with it i thnik its not worth testing with 2 frameworks.

btw ts-mockito support testing components

anyway thanks for providing answer i though im missing sth simple.

@mike-hanson
Copy link
Contributor

Argument accepted, but the whole philosophy behind NSubstitute is that the formal differences between, Mock, Stub, Fake, Spy etc don't matter if it lets you substitute a real element (class, method, function etc) with something else to enable testing then that is what matters.

Whilst I welcome the ability to work with JS/TS as if it were strongly typed it isn't and I prefer to embrace that we can do some things more easily because of the dynamic nature of the language, and for me Spies are an acceptable compromise and qualify as a form of Substitute.

I looked at ts-mockito and rejected it because like all similar frameworks other than NSubstitute/Substitute they force you to write messy ugly test code. The elegance of the NSubstitute approach for me is so much more natural and readable. So I am really happy that this library exists, and hope it doesn't get drawn into copying patterns from other libraries.

@Styrna Styrna closed this as completed Apr 2, 2019
@Karql
Copy link
Contributor

Karql commented Apr 18, 2019

Hi @ffMathy

At the begining I'd like to thank you for great work you've done! ;)

I would like use it with angular project and run into the same issue.

After some debugging the problem is with angular JIT compilation.

Sample:

  mockSampleListService.getSampleList().returns(of(list));

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [ SampleListComponent ],
      providers: [
          { provide: SampleListService, useValue: mockSampleListService }
      ]
    })
    .compileComponents();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(SampleListComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

When anuglar try to createComponent execute:

https://github.com/angular/angular/blob/d68a98f0cd7e029f87941e8c9dbf07423aaf54b7/packages/compiler/src/compile_metadata.ts

  function identifierName(compileIdentifier: CompileIdentifierMetadata | null | undefined):
    string|null {
    if (!compileIdentifier || !compileIdentifier.reference) {
      return null;
    }
    const ref = compileIdentifier.reference;
    if (ref instanceof StaticSymbol) {
      return ref.name;
    }
    if (ref['__anonymousType']) {
      return ref['__anonymousType'];
    }
    let identifier = stringify(ref);
    if (identifier.indexOf('(') >= 0) {
      // case: anonymous functions!
      identifier = `anonymous_${_anonymousTypeIndex++}`;
      ref['__anonymousType'] = identifier;
    } else {
      identifier = sanitizeIdentifier(identifier);
    }
    return identifier;
  }
 if (ref['__anonymousType']) {
      return ref['__anonymousType']; // this part return Proxy
    }

And this proxy is casting to string.
This cause error:
TypeError: Cannot convert object to primitive value

I have two ways to resolve it:

  1. Run test without JIT
    TestBed.configureCompiler({useJit:false});

  2. Configure __anonymousType to return something
    mockSampleListService['__anonymousType'].returns('SubstituteJSFake'); // without brackets { } this name is use as function arg name

@ffMathy maybe is good idea to add framework specify wrappers like
NgSubstitute which will be set neccssary stuff?

or maybe is better way to accomplish this?

Best regards!

@ffMathy
Copy link
Owner

ffMathy commented Apr 20, 2019

Great idea @Karql!

@ffMathy ffMathy reopened this Apr 20, 2019
@Karql
Copy link
Contributor

Karql commented Apr 20, 2019

@ffMathy for my purposes I've done something like this:
https://github.com/Karql/angular-tests/blob/0c8f6461022f6a8a84379d14a8e454999fa35806/libs/testing/src/lib/wrappers/ng-substitute.wrapper.ts

Set substitute['__anonymousType'].returns('SubstituteJSFake'); it is a shourtcut but it works :P

Properly this property should be set to undefined but then promise is pass to stringify method.
https://github.com/angular/angular/blob/master/packages/core/src/util/stringify.ts

So next step will be set returns for "overridenName", and "name".
Personally I don't want do that because "name" is very common property.

I hope this info will help you a bit ;)

@ffMathy
Copy link
Owner

ffMathy commented Apr 29, 2019

It sure does @Karql! I'll take a look as soon as I have the time. Won't be for the next month or so I think.

I think I'll end up creating different integrations for substitute.js, for instance @fluffy-spoon/substitute-angular, which you can then "hook in" to substitute.js to support these cases.

@DamienBraillard
Copy link

I got a workaround that allows to inject substitutes as providers using the TestBed.

Defining the substitute provider resolution by value ('useValue') like this does not work indeed:

//
// Not working example because the provider is resolved by 'useValue'
//
describe('My test', () => {
  let sampleListService: SubstituteOf<SampleListService>();

  beforeEach(() => {
      sampleListService = Substitute.for<SampleListService>();

      TestBed.configureTestingModule({
        declarations: [ SampleListComponent ],
        providers: [
            { provide: SampleListService, useValue: sampleListService }
        ]
      })
      .compileComponents();
    });
});

But... what works is to setup the provider resolution by factory like that:

//
// Working example because the provider is resolved by 'useFactory'
//
describe('My test', () => {
  let sampleListService: SubstituteOf<SampleListService>();

  beforeEach(() => {
      sampleListService = Substitute.for<SampleListService>();

      TestBed.configureTestingModule({
        declarations: [ SampleListComponent ],
        providers: [
            { provide: SampleListService, useFactory: () => sampleListService }
        ]
      })
      .compileComponents();
    });
});

For the sake of brievety:

Replace

{ provide: SampleListService, useValue: sampleListService }

By

{ provide: SampleListService, useFactory: () => sampleListService }

And it should work. I'm using Jest as test framework with Angular 7.

Hope it helps !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

7 participants