ember.js: default component unit test is broken for glimmer components
It seems the component unit test template doesn’t make any distinction between whether it is testing glimmer vs classic components, but it only actually works for classic components
The generated test (from this template)
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module('Unit | Component | my-glimmer-component', function(hooks) {
setupTest(hooks);
test('it exists', function(assert) {
let component = this.owner.factoryFor('component:my-glimmer-component').create();
assert.ok(component);
});
});
results in Failed to create an instance of 'component:my-glimmer-component'. Most likely an improperly defined class or an invalid module export.
Full traceback:
Died on test #1 @http://localhost:4200/assets/tests.js:344:21
processModule@http://localhost:4200/assets/test-support.js:3885:16
module$1@http://localhost:4200/assets/test-support.js:3910:17
@http://localhost:4200/assets/tests.js:342:21
Module.prototype.exports@http://localhost:4200/assets/vendor.js:118:32
requireModule@http://localhost:4200/assets/vendor.js:39:18
require@http://localhost:4200/assets/test-support.js:14293:16
loadModules@http://localhost:4200/assets/test-support.js:14286:14
loadTests@http://localhost:4200/assets/test-support.js:15306:22
start@http://localhost:4200/assets/test-support.js:14983:33
@http://localhost:4200/assets/tests.js:337:25
Module.prototype.exports@http://localhost:4200/assets/vendor.js:118:32
requireModule@http://localhost:4200/assets/vendor.js:39:18
@http://localhost:4200/assets/tests.js:539:8
: Failed to create an instance of 'component:my-glimmer-component'. Most likely an improperly defined class or an invalid module export.
Source: | create@http://localhost:4200/assets/vendor.js:1213:15 @http://localhost:4200/assets/tests.js:345:78 runTest@http://localhost:4200/assets/test-support.js:5735:30 run@http://localhost:4200/assets/test-support.js:5721:13 runTest/<@http://localhost:4200/assets/test-support.js:5952:12 processTaskQueue@http://localhost:4200/assets/test-support.js:5308:24 advanceTaskQueue@http://localhost:4200/assets/test-support.js:5293:20 advance@http://localhost:4200/assets/test-support.js:5279:4 unblockAndAdvanceQueue@http://localhost:4200/assets/test-support.js:7083:20 begin@http://localhost:4200/assets/test-support.js:7117:5 internalStart/config.timeout<@http://localhost:4200/assets/test-support.js:6349:6
The failure is specifically with this line, where it tries to invoke .create() on the non-ember-object glimmer component class
It seems it should be doing something like this instead:
module('Unit | Component | my-glimmer-component', function(hooks) {
setupTest(hooks);
test('it exists', function(assert) {
let { class: componentClass } = this.owner.factoryFor('component:my-glimmer-component');
let componentManager = this.owner.lookup('component-manager:glimmer');
let named = {}; // your args
let component = componentManager.createComponent(componentClass, { named });
assert.ok(component);
});
});
More info (and thanks) in this blog post
I’m happy to submit a PR for this, but unsure what the behavior should be. A few options:
- change the blueprint to only work with Glimmer components (unclear if having newly-generated tests not work for classic components would be considered a “breaking change”?)
- figure out some way to detect which type of component is referred to while generating the test, and produce the correct output
- do detection of which component type we’re dealing with at runtime (likely complicating the test)
- accept a flag to switch between component test types
- create a separate
glimmer-component-testblueprint - figure out some way to create components that works regardless of type
I’d probably be inclined to go with 4., with default being glimmer, but curious to hear what others think (particularly about whether there’s a workable way to do 2. or 6.)
About this issue
- Original URL
- State: closed
- Created 4 years ago
- Reactions: 3
- Comments: 20 (7 by maintainers)
OK, seriously though: The patterns that exist that let you gain access to the component instance from your rendering tests are expressive enough for these use cases without forcing component’s to be instantiated in a completely foreign and unnatural way.
For example:
This pattern can easily be moved into something more easily shared if you have more than one test that uses it:
@gabrielgrant yes that’s what I’ve done too, and there definitely are situations where this is beneficial. There are also others where it’s not. The choice of whether to use a functional approach or a stateful OOP approach should be left to the developer, or at least consistent within the framework, and not forced upon depending on whether one needs to write tests or not
Essentially, the “Ember way” prescribed in guides:
@tracked!BUT… you want to write tests?
The situations where doing this is unwieldy are when the component has complex state due to its args and tracked properties, but then you have simultaneously force a “stateless” approach on it by having a bunch of getters call extracted functions:
It adds no value because it is actually very dependent on state, but then the code is all over the place just for the sake of tests
👍👍 going to close now, as I think my suggestion in https://github.com/emberjs/ember.js/issues/18877#issuecomment-675758868 is the path forward when you absolutely must have this functionality.
One more option - using debug API - https://gist.github.com/lifeart/0484b374a3712d96f393cdd5d7c9cd49
To be clear, I think rendering tests are great. I simply meant it’s not the right tool for every job and as a work around we’re being asked to use a much more cumbersome alternative.
The solution you provided is far more reasonable. It should suit most use cases and make it easier to migrate existing unit tests. Thanks!
@gabrielgrant I did hear what you were saying earlier and I understand that, and I’m not refuting your good experience. However just because it turned out good in your case doesn’t mean it’s good in all cases. Please don’t invalidate other people’s experiences because it happened to to turn out good for your personal situation
Do all your component getters call separate, external functions? I’m assuming not – because in some cases it doesn’t make sense to. In your case it was a better refactor AND it allowed it to be tested. I’m saying there are cases where extracting it out allows it to be tested BUT is not a better refactor because of the dependency on args and states.
You changed your code to allow it to be tested, and coincidentally that was a better refactor – that’s great. However, the decision to extract code into functions should be separate from whether code can be tested or not, because in some cases this is not a better refactor:
Imagine having to do this for more than one getter. There is no meaningful refactor here by extracting the code. If I had multiple getters written this way, the first question someone would ask reading the code would be “why are these extracted?” and the response “just so they can be tested” would undoubtedly illicit a facepalm
I would want to just use an integration test and be done with this argument but I can’t. Would love to see how you would handle this.
@wongpeiyi agree that creating a bunch of these stub methods can be annoying in a few cases, but in general, this separation is precisely what i’ve found useful: the tests for how that stateful data gets initially computed/used in computation has unit tests on stateless functions; how the state gets established and how the results of those computations are used is tested through simple rendering tests.
In what you’re calling the “OOP” approach, testing the computation method on a component requires the test to know about, and properly set up, all the required internal state of the component. This kind of poking around internal object state from a test feels like it creates an unnecessary coupling between test and implementation. The stateless computation approach keeps those implementation details out of the tests – the knowledge of what state is used in a given method doesn’t need to leak outside of the method’s own implementation, and tests can focus on testing behavior
@schleifer-john the discussions we’ve had on the core team generally come back to “what is the ‘unit’ you are testing”. This is also why “integration” tests were renamed to “rendering” tests - the logic is that standalone component rendering tests are unit tests, because they are testing the most fundamental unit of code.
One of the common antipatterns we’ve seen, for instance, is users unit testing a component on its own without considering its interactions with any of its child components. You may say that makes sense though, because that would be an integration - but when you look at a component as a function that receives arguments and produces HTML, then that assumption kind of changes. If you were unit testing the following function:
You wouldn’t mock the
someOtherUtilityandyetAnotherUtilityfunctions so that you’re testingsomeUtilityon its own, because you then have a test that is testing too little - it’s effectively meaningless. The same goes for testing without considering lifecycle, which is pretty fundamental to components.All that said, I do think there are places for testing business logic independent of rendering. There are a few options there:
I’ll raise your concerns again with the core team as well to keep the conversation going, I do want to make sure your concerns are addressed and your team feels like they have a solid, well thought out path forward.
@pzuraq I know the speed of running the test case suite has gotten better now, but historically running integration tests was much slower performance-wise vs unit tests. I believe this is what originally led us down the route of favoring unit tests vs integration tests at the component level.
I also come from more of a Java / Spring background where Unit Tests are more ingrained into the regular development process, so hearing that there aren’t use cases for being able to isolate and test units of code seems like a radical deviation from what the rest of the software industry as a whole is doing.
Willing to give integration tests in Glimmer components a try and I can see where only having integration tests could work very well for simple components, but I don’t think this “one-size-fits-all” approach works for more complex components.
There are plenty of articles out there explaining the benefits of Unit Testing and I believe the same applies here for components. For more complex components in particular, we need the ability to isolate and test specific logic at the unit level without having to wire up and render the entire component. Maybe integration tests could work in these cases too, but the perception here is that it feels like the Ember framework is losing features / functionality.