getsaf / shallow-render

Angular testing made easy with shallow rendering and easy mocking. https://getsaf.github.io/shallow-render
MIT License
273 stars 25 forks source link

Is it possible to test the Component in isolation without the HTML template? #203

Closed azaeng04 closed 3 years ago

azaeng04 commented 3 years ago

It seems that TestBed.configureTestingModule({ provider: MyComponent }).createComponents() is being called for each shallow render. Is it possible to call it without TestBed.configureTestingModule({ provider: MyComponent }).createComponents()? So it will look like this: TestBed.configureTestingModule({ provider: MyComponent })

I quite like the mocking ability with this library so it would be great if I could not call .createComponents() in my tests. This is so I can test the component in isolation.

azaeng04 commented 3 years ago

Basically trying to achieve something like this:

let myComponent: MyComponent;
TestBed.configureTestingModule({
    providers: [
        MyComponent
    ],
});
myComponent = TestBed.inject(MyComponent);
getsaf commented 3 years ago

Yes, it's possible (and is also my preferred method of testing).

See the Optional Templates section of the docs:

As well as some examples of no-template rendering tests

azaeng04 commented 3 years ago

@getsaf looking at those examples it seems like they are in fact rendering the component with the HTML. Is there a more advanced example of this?

getsaf commented 3 years ago

it seems like they are in fact rendering the component with the HTML

I'm not sure what you mean by this. The test examples I linked to definitely do not use HTML templates to render the components, only inputs. I feel like I'm misunderstanding the question here.

  it('displays and tracks the name', async () => {
    const { find, outputs } = await shallow.render({
      bind: { name: 'Chuck Norris' },
    });
    const label = find('label');
    label.nativeElement.click();

    expect(label.nativeElement.textContent).toBe('Chuck Norris');
    expect(outputs.selected.emit).toHaveBeenCalledWith('Chuck Norris');
  });

Those input variables aren't even required, you can just call render without any arguments at all if you want:

  await shallow.render();
azaeng04 commented 3 years ago

@getsaf Reason why I say it doesn't look like it is cos you are referencing label.nativeElement.textContent. Which is an HTMLElement, if I am not mistaken, in that example?

All I am interested in is the Component with the methods and just doing a basic class test on the @Component methods

Example:

import { TestBed } from "@angular/core/testing";
import { MyComponent } from "./my-component";
import { MyService } from "./my-service";

describe('MyComponent', () => {
    let myComponent: MyComponent;
    let myService: MyService;

    beforeEach(async () => {
        TestBed.configureTestingModule({
            providers: [MyComponent, MyService]
        }); // --> I am not calling .compileComponents() which binds the HTML
        myComponent = TestBed.inject(MyComponent); // --> inject an instance of the component under test
        myService = TestBed.inject(MyService);
    });

    test.only('should call onClick method once', async () => {
        jest.spyOn(myService, "getResponse").mockReturnValue({
            total: 10000
        });
        jest.spyOn(myComponent, "onClick");

        myComponent.myMethod(); // call myMethod that I want to test

        expect(myComponent.onClick).toHaveBeenCalledTimes(1);
    });
});

Passing the component as one of the providers and its relevant services and executing the methods inside myComponent like myComponent.myMethod()

Is this something that can be done with shallow.render()?

getsaf commented 3 years ago

You have access to the component in the render response by using the instance property.

  const {instance} = await shallow.render();
  instance.myMethod();

I will say that when you test component class methods directly, you would not verify the DOM interactions are hooked up properly in the HTML template. This method is usually considered to be testing the implementation rather than the behavior and generally leads to coupling your tests to the specific way in which the component class implements a behavior without actually testing the behavior which can make it difficult to refactor a component in the future. /soapbox

Either way though, sometimes accessing the component instance directly is the only way to go so the instance is there if you need it!

azaeng04 commented 3 years ago

Thanks so much @getsaf I realized that when I need to provide the Renderer2 as a dependency. I get an eslint error. How do I work around this?

Screenshot 2021-08-10 at 13 35 21

getsaf commented 3 years ago

The console in your screenshot says that the Renderer2 class is abstract, those can't be used as Angular providers. I think Renderer2 is an internal Angular thing that you should not need to provide it manually. I'm also pretty sure you don't need to use provideMock in your shallow options.

IMO, rendering the component in a beforeEach is likely gonna cost you more code/headache than just calling shallow.render() it in each test because you lose access all the built-into shallow-render tools like mocking/query/fixture, etc when you render before your test executes.

Just suggestions here, but this kind of refactor could save you time and is more in-line with shallow-render's intended use:

let shallow: Shallow<MyComponent>;
beforeEach(() => {
  // Only define your baseline setup but do not render here...
  shallow = new Shallow(MyComponent, MyModule);
});

it('displays the service total when the button is clicked', async () => {
  const { find, fixture } = await shallow
    .mock(MyService, { getResponse: () => ({ total: 10000 }) })
    .render();

  find('button').click();
  fixture.detectChanges();

  expect(find('label').nativeElement.innerText).toContain('Response was 10,000');
});
azaeng04 commented 3 years ago

Thanks so much @getsaf!

See the Renderer2 is included as a dependency. constructor MyComponent(private Renderer2) So when running the tests. The TestBed complains that it is not being provided. How do I work around this with shallow.render()?

Thanks for the tip! I am still just playing around with the lib to get an idea of how it works.

Side note: I would love to invite you to this Angular community I recently joined: https://www.angularnation.net/. It would be great if you could do a talk on this amazing lib you wrote. I find it REALLY helpful and I am sure other devs would too!