To stub or not to stub the router service in integration tests

Let’s imagine a component uses the router service currentRouteName property to decide what to display in its template.

To tests this, I would write an integration test where I would stub the router service and provide it with a dummy currentRouteName value.

But in a recent comment on a related topic https://github.com/ember-cli/ember-cli-qunit/issues/203#issuecomment-506735392, @rwjblue suggested that we should not stub the router service.

If we don’t stub the router service, then what is the right way to write this integration test?

Thx for the insight

Ju

It might be helpful if you shared a basic gist of the component and test here? I’m happy to try to answer, but its a bit too abstract at the moment…

We have a similar use-case but @rwjblue is right, the description is very abstract. In our case we have a component which uses the router service to transition to another page if an error occurs.

In the test, we didn’t want to do the actual transition. In the component test, we only want to assure that the component calls transitionTo with the correct parameters.

So in our case we mocked the router and put a spy on the transitionTo method of the mocked object.

Maybe there is a smarter way but it works for us and we do not need to care about all the stuff which would be triggered by a transitionTo.

Sorry for the abstract description. Here is a less abstract example:

// component.js
import Component from "@ember/component";
import { service } from '@ember-decorators/service';

export default class MyComponent extends Component {
  @service router;
}

{{!-- template.hbs --}}
<p>
  This component might be rendered in many different routes ;
</p>

{{#if (eq this.router.currentRouteName "my-special-route")}}
  <p data-test-special-route-only-content>
    but I want to show this only on a particular route
  </p>
{{/if}}

When testing MyComponent, this.router.currentRouteName needs to be set, I this is how I do it:

// component-test.js
import { module, test } from "qunit";
import { setupRenderingTest } from "ember-qunit";
import { render } from "@ember/test-helpers";
import hbs from "htmlbars-inline-precompile";
import Service from '@ember/service';

module("Integration | Component | <MyComponent />", function( hooks ) {
  setupRenderingTest(hooks);

  module('when rendering on any route', function(hooks) {
    hooks.beforeEach(function() {
      const routerStub = Service.extend({currentRouteName: 'dummy'},);
      this.owner.register('service:router', routerStub);
    });

    test("its", async function(assert) {
      await render(hbs`<MyComponent />`);
      assert
        .dom("[data-test-special-route-only-content]")
        .doesNotExist();
    });
  });

  module('when rendering on the special route', function(hooks) {
    hooks.beforeEach(function() {
      const routerStub = Service.extend({currentRouteName: 'my-special-route'},);
      this.owner.register('service:router', routerStub);
    });

    test("its", async function(assert) {
      await render(hbs`<MyComponent />`);
      assert
        .dom("[data-test-special-route-only-content]")
        .exists();
    });
  });
});

If stubbing the router service is a bad practice, I would like to know what is the alternative testing strategy.

Thx

We have a tab component which uses the router service to determine the current route so we can set the tab as “active”. Right now, I’m not able to write an integration test for it, so I’ve ended up using an acceptance test for it.

In a nutshell, this is what I’m trying to do:

<UiTabs as |Tabs|>
  <Tabs.tab @href="/foo">
    Tab One
  </Tabs.tab>
  <Tabs.tab @href="/bar">
    Tab Two
  </Tabs.tab>
  <Tabs.tab @href="/baz">
    Tab Three
  </Tabs.tab>
</UiTabs>
1 Like

I’m still interested in hearing what the alternative is to mocking the router service, but so far I’ve had good success with mocking it so long as I reregister the original instantiated router service afterEach.

1 Like

I think in this case you should prefer an acceptance test which is closer to reality anyway.

An integration test with a mocked service would be brittle and might require more maintenance than expected but maybe that’s an acceptable amount of overhead for you in this case.

I think it should be totally acceptable to mock a service for an integration test. That’s half the point of dependency injection in the first place isn’t it?

To say that a service can’t be mocked if it’s already instantiate is fine, but IMO should be a problem with the framework not the user, IMO.

It’s generally an edge case that this happens, except with the router service, since the user can’t control when it gets instantiated.

IMO, it should be perfectly valid to want to stub out transitionTo or currentRouteName in an integration test, but even if there is disagreement about that, i don’t think the solution should be “you’re doing it wrong”.

1 Like

If anyone is stuck with it i do that in my integation test component that check the current route:

only('The cron passed on argument is setting up the form', async function(assert) {
let router = this.owner.lookup('router:main')
router.currentRouteName = 'client.triggers.edit'
router.setupRouter()

await render(hbs`<TriggerForm::Cron @cron='0 2 3 ? * 2,4,5 *'/>`)
2 Likes

:wave: That solution doesn’t work with the recommended rule no-private-routing-service

const router = this.owner.lookup('service:router');
this.owner.setupRouter(); 

router.set('currentRouteName'... // cannot set read only property

Hello everyone. I guess we still not have an answer for it right?

I read in this issue: https://github.com/ember-cli/ember-cli-qunit/issues/203 @rwjblue saying that we should not mock the service:router right?

I have a component that uses the urlFor method from this service and when in Integration Test this call throw this error:

Source: TypeError: Cannot read property ‘generate’ of undefined at Class.generate (confidentialProject/assets/vendor.js:47687:38) at RouterService.urlFor (confidentialProject/assets/vendor.js:43832:27)

Is this supposed to happen in a Component Test?

Just to complete the information: I’m in Ember 3.8 (in the middle of an upgrade process that targets Ember 3.20)

I would stub the service if you’re trying to use a specific method on the service. I’ve run into issues trying to use the actual router service, especially with setupRouter, but if you’re just stubbing it for urlFor that should work well.

EDIT: i see there is more debate than I thought around this issue which seems crazy to me but :man_shrugging:. A large part of the issue seems to be around instance initializers though. I would still try stubbing the router service and see where you can get. If that doesn’t work you could try the lookup and “setupRouter” method but that seems more issue prone to me.

I think you didn’t manage it because you was looking for the service, but @kali answer is saying to get the real router:

let router = this.owner.lookup('router:main')

On my sidenav menu integration test I sucessfully used it. An important step was to use await settled(); to make sure of things. So I sucessfully manage to test menu activations like:

test('The current option is highlighted', async function(assert) {
    let router = this.owner.lookup('router:main');
    set(router, "currentRouteName", 'admin.index'); //initial route
    router.setupRouter();

    await render(hbs`<AdminSidenav @user={{this.model}} />`);
    assert.dom('[data-test-menu-home] a').hasClass('active');

    set(router, "currentRouteName", 'admin.config.show'); // change route to config
    router.setupRouter();
    await settled();
   
    assert.dom('[data-test-menu-config] a').hasClass('active');
    

    ....
});

If you’re using sinon, it’s able to stub getters

sinon.stub(this.owner.lookup('service:router'), 'currentRouteName').value('foo');
2 Likes

This is super helpful, thanks

1 Like