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.
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>
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.
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”.
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)
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 . 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');
....
});