Angular-cli: Spy on jasmine function from an Angular library is not working

Created on 12 Mar 2020  路  18Comments  路  Source: angular/angular-cli

I have an Angular library containing functions like "export function myFunction".
The project with the error have this library as a dependency and when I want to spy on the function, the error below is display:

myFunction is not declared writable or has no setter

Now the real world example:

A simple function from the library:

export function isNotEmptyString(value: any): value is string {
  return _.isString(value) && !_.isEmpty(value);
}

The dts of this function once packaged:

export declare function isNotEmptyString(value: any): value is string;

To spy on the function, I must have an object at some point.
So I use a custom module import to achieve this.

The spy on error:

import * as MyLibfrom 'my-lib';

const isNotEmptyStringSpy = spyOn(MyLib, 'isNotEmptyString').and.returnValue(false);

And the error is:

isNotEmptyString is not declared writable or has no setter

Now, if I use a spyOnPropety:

import * as MyLibfrom 'my-lib';

const isNotEmptyStringSpy = spyOnProperty(MyLib, 'isNotEmptyString').and.returnValue(() => false);

And the new error is:

isNotEmptyString is not declared configurable

I also tried to use differents module inside the tsconfig.json compiler options.
Like commonjs, CommonJS, ES2015 and ESNext.
There is no difference.

tsconfig.json

"compilerOptions": {
  "module": "commonjs"
}

Does anyone have a suggestion?
I am stuck with this :/

Thanks !

Environment:

"karma": "4.4.1"
"jasmine-core": "3.5.0"
"@angular/core": "9.0.5"

All 18 comments

I'm sorry, but this issue is not caused by Angular CLI.

Please see the following Jasmine issue: https://github.com/jasmine/jasmine/issues/1414

Hi, I'm a Jasmine developer. I was able to reproduce this. It looks like the property descriptors that Angular 9 generates for module exports are marked read-only. Specifically, they have a getter but no setter and have configurable: false. As a result, it's not possible to overwrite the property with a spy.

I don't think there's anything Jasmine can do to fix this. We can only play by the rules of the language, and if the rules say that a property can't be set then we can't set it.

@C0ZEN Your best bet is probably to use dependency injection for anything that you want to spy on, rather than trying to spy on module exports.

@sgravrock thank you for your time and sharing this information.
Do you think that Angular could provide a way to generate configurable: true or should I just take that other path and refactor all my tests ?

Do you think that Angular could provide a way to generate configurable: true or should I just take that other path and refactor all my tests?

These are actually not emitted by the Angular compiler but rather TypeScript and Rollup. Also, in this case it wouldn't be correct to change the the module exports definition to be configurable, because that shouldn't change.

I do agree with @sgravrock, that you should use DI if you want to spyOn.

@alan-agius4 ok, fine, so I must find a way with the dependencies as they are.
Nonetheless, are you talking about Angular's DI ?
Because pure functions, as well as I am aware of, have nothing to do with the DI since there is no decorator to make them available as tokens.

I am having the same problem. We basically have a utility package that has pure functions and we can't spy on them. We can't complete the coverage because of this.

@C0ZEN , have you found a solution?

@Skullpluggery I have found a workaround (which I do not like but I had to move forward with ng9, so no choice here).
I changed my way to test.
I removed all the spies on pure functions coming from the library.
So basically, instead of writing unit tests with a bunch of spies to control the behavior, I removed them and let the real behavior of the library be itself.
Hear me out, this is good thing for most of the cases, but sometimes I prefer to use a spy and now this is no longer possible.

For an Angular CLI built application, construction of the namespace object is performed by Webpack within its runtime.

A module namespace object is considered an exotic object by the ES standard and has very specific behavior (each of the internal methods are customized). The relevant section is found here: http://www.ecma-international.org/ecma-262/6.0/#sec-module-namespace-exotic-objects
Of note in that section is that each export property should be writable. The generated Webpack namespace object appears to create accessor property descriptors instead of data property descriptors (ref: http://www.ecma-international.org/ecma-262/6.0/#sec-module-namespace-exotic-objects-getownproperty-p). However, even if this were changed, namespace objects are not intended to be mutated and other bundlers (such as rollup) also do not allow this behavior. Some type of import mocking setup could also be an option for this scenario.

Adding "module": "commonjs" in tsconfig.spec.json lets you spyOn exported functions in angular.

1.abc.spec.ts
import * as MyLibfrom 'my-lib';

const isNotEmptyStringSpy = spyOn(MyLib, 'isNotEmptyString').and.returnValue(false);

  1. tsconfig.spec.json
    "compilerOptions": {
    "outDir": "../out-tsc/spec",
    "module": "commonjs",
    "target": "es5",
    "types": ["jasmine", "node"]
    }

Note that you need to add module in tsconfig.spec.json not in tsconfig.json.

@C0ZEN if the function is pure you are ok.

But how would you control a random() function for example?

And what if you want to test how many times a function was called or the params it was called with?

I also had @shwetasingh237 solution and works great for testing.

@shwetasingh237 @gkamperis either Angular/jasmine has changed (highly improbable) and the problem is gone or you are just not using a good reproduction of the problem.

Just a quick reminder of the problem:

  • Create a library with pure functions and packaged by ng-packagr
  • Consume the library in an Angular application
  • Spy a pure function from the lib

Changing the module to commonjs is a good solution INSIDE a library nevertheless this is no longer working OUTSIDE of the library.
Please @shwetasingh237, could you confirm that you are testing in this context because if this is true, then a "good" workaround is now possible and this would be awesome.

@C0ZEN I don't understand why the setup is important.

I had the same issue going to ng9 with tests that used jasmine.spyOn() on a local module function and on other 3rd party package functions (is purity is important in this case?).

Also what is the difference between your own module function and an external ng-packagr packaged function? spyOn works the same.

@gkamperis well I am just the dude reporting the issue. I can not tell you more than this is not working for me and it seems that it will stays that way.
If you read above the comments you will figure out that Angular add the configurable: false and this seems the right thing to do.
Jasmine is then not able to Spy the function and here we are with a breaking change on the tests with ng9 and the only smart thing to do is find a workaround or change the way to test.

@C0ZEN understood.

But the compilation result is dependent on the way the "module" property works and setting it to commonjs for testing is a workaround that does allow to use spyOn().

@gkamperis as I said, this is not working anymore with ng9.
This is explained within the issue itself.

I also tried to use differents module inside the tsconfig.json compiler options.
Like commonjs, CommonJS, ES2015 and ESNext.
There is no difference.

@C0ZEN it is working for me.

I think you need to read again @shwetasingh237 's comment.

You have two different tsconfigs. One for app building and one for specs.
The module property is different in each.

@gkamperis I do have a different target between my tsconfig files and I am obviously speaking about the spec one.
I have a reproduction (updated to ng 9.1.x) with commonjs module and es5 target and this is still not working.

Like

Error: <spyOn> : isNotEmptyString is not declared writable or has no setter
Usage: spyOn(<object>, <methodName>)

With

import * as MyModule from 'my-lib';
spyOn(MyModule, 'isNotEmptyString').and.returnValue(false);

This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

_This action has been performed automatically by a bot._

Was this page helpful?
0 / 5 - 0 ratings

Related issues

MateenKadwaikar picture MateenKadwaikar  路  3Comments

IngvarKofoed picture IngvarKofoed  路  3Comments

delasteve picture delasteve  路  3Comments

ericel picture ericel  路  3Comments

JanStureNielsen picture JanStureNielsen  路  3Comments