Let's say I have the following files/code:
Person.ts
export class Person {
id: string;
firstName: string;
lastName: string;
isEmployed: boolean = true;
isManager: boolean = false;
public static Name = ():string => this.firstName + ' ' + this.lastName;
}
WorkHistory.ts
import { Person } from './person';
export class WorkHistory {
public propA: string;
public propB: string;
public getHistory = (p: Person): any => {
// do something
// return history
};
}
Formatter.ts
import { Person } from './person';
import { WorkHistory } from './workHistory';
export class Formatter {
public formatWork(p: Person) {
let wh: WorkHistory = new WorkHistory();
let whData = wh.getHistory(p);
// do formatting
// return formatting
}
}
I'm trying to write a unit test for the formatWork method. However, I can't figure out how to stub out WorkHistory and its properties.
Here's what I've got so far:
Formatter.spec.ts
describe('formatWork', () => {
let mockWorkHistory = {
propA: '',
propB: ''
};
let sandbox;
let formatter;
beforeEach(() => {
sandbox = sinon.createSandbox();
sandbox.stub(WorkHistory, "prototype").value(mockWorkHistory);
});
afterEach(() => {
sandbox.restore();
});
it('should do something', () => {
// create person object
formatter = new Formatter();
var result = formatter.formatWork(person);
console.log(result);
});
});
I've tried stubs and sandbox.replace, however, I can't seem to overwrite WorkHistory default properties or methods.
As it stands, the above throws an exception in Phantom 2.1.1:
TypeError: Attempting to change enumerable attribute of unconfigurable property.
Additionally, the console.log in my fixture shows all of the default properties for WorkHistory instead of the overwritten values.
What am I missing? What am I doing wrong?
I ended up having to use ts-mock-imports since I was using webpack. Thanks!
The Sinon only can't cover all your requirements. You need some tools to mock your import, like genMockFromModule in Jest or proxyquire
Sinon is a pretty convenient way to generate stub, but the tool knows nothing about your module structure. And you should find a way how to pass your Sinon-mocked class to the module that you are testing.
Related
I have this sample of code experimenting with mixins in TypeScript. However, it is not returning what I am expecting.
It should give me: User ({"id":3,"name":"Lorenzo Delaurentis"}).
Instead, I am getting: Function ({"id":3,"name":"Lorenzo Delaurentis"}).
The line let Name = Class.constructor.name should give me User, but it is not. Am I missing something obvious here?
type ClassConstructor<T> = new(...args: any[]) => T
function withDebug<C extends ClassConstructor<{
getDebugValue(): object
}>>(Class: C) {
return class extends Class {
constructor(...args: any[]) {
super(...args)
}
debug() {
let Name = Class.constructor.name
let value = this.getDebugValue()
return `${Name} (${JSON.stringify(value)})`
}
}
}
class DebugUser {
constructor(
private id: number,
private firstName: string,
private lastName: string
) {}
getDebugValue() {
return {
id: this.id,
name: `${this.firstName} ${this.lastName}`
}
}
}
let User = withDebug(DebugUser)
let user = new User(3, 'Lorenzo', "Delaurentis")
console.log(user.debug())
P.S. I compiled with tsc mixins --target ES6. Otherwise, I get an error: error TS2339: Property 'name' does not exist on type 'Function'.
You want just Class.name. The Class.constructor is Function.
Is there some npm package for memoizing object lazily, so that the first attempt to access it would load it?
The problem:
// service
class Service {
private readonly pathMap = {
user: process.env.USER_PATH,
post: process.env.POST_PATH,
page: process.env.PAGE_PATH,
}
getPath(entityType: EntityType) {
return this.pathMap[entityType];
}
}
export const service = new Service();
// service.spec.ts
import { service } from './service';
import { loadEnvVars } from '#app/loadEnvVars';
describe('service', () => {
beforeAll(loadEnvVars);
it('should return path', () => {
expect(service.getPath('user')).toBe(process.env.USER_PATH);
expect(service.getPath('post')).toBe(process.env.POST_PATH);
expect(service.getPath('page')).toBe(process.env.PAGE_PATH);
});
});
The tests will fail because the singleton service will load before the loadEnvVars due to the import of the service (before the beforeAll), which means the env vars will be set to undefined in the service pathMap.
The proposed solution:
I know there are several ways to fix it, but IMO the best solution would be to somehow lazy load the pathMap object so that the first attempt to get something from it will actually init the variable's value.
Here's a function I wrote to tackle this (Typescript):
export function lazy<T extends (...args: any[]) => any>(factory: T): ReturnType<T> {
let obj: ReturnType<T> | undefined;
const proxy = new Proxy(
{},
{
get(_, key) {
if (!obj) {
obj = factory();
}
return obj[key];
}
}
);
return proxy as ReturnType<T>;
}
Now the service will look like this instead:
class Service {
private readonly pathMap = lazy(() => ({
user: process.env.USER_PATH,
post: process.env.POST_PATH,
page: process.env.PAGE_PATH,
}));
getPath(entityType: EntityType) {
return this.pathMap[entityType];
}
}
export const service = new Service();
Now the tests will pass.
Note: In this solution lazy returns a read-only object. It can be changed of course.
The question:
Is there some NPM library out there that provides something like that already? Cause if not I think I might publish it myself.
I want to write a constructor function that takes some predefined methods from an object (Methods) and injects it into every new object that is created with the constructor. It injects methods from another object because, I want the consumers of my module to be able to add new methods.
But the problem is: as all the injected methods are not defined in the constructor I can't seem to manage their type annotations properly.
It's hard for me to describe the problem so I created a simple example (with JavaScript to avoid all the type error) to demonstrate it.
// methods.js ---------------------------------------
const methods = {};
const addMethod = (name, value) => (methods[name] = value);
// these methods should be added by an external user of the programmer.js module
function code(...task) {
this.addInstruction({
cmd: "code",
args: task
});
return this;
}
function cry(...words) {
this.addInstruction({
cmd: "cry",
args: words
});
return this;
}
addMethod("code", code);
addMethod("cry", cry);
// programmer.js -------------------------------------
const retriveInstructionsMethod = "SECRET_METHOD_NAME";
const secretKey = "VERY_SECRET_KEY";
function Programmer() {
const instructions = [];
this.addInstruction = (value) => instructions.push(value);
Object.defineProperty(this, retriveInstructionsMethod, {
enumerable: false,
writable: false,
value(key) {
if (key === secretKey) return instructions;
},
});
for (const key of Object.keys(methods))
this[key] = (...args) => methods[key].apply(this, args);
}
// test.js -------------------------------------------
const desperateProgrammer = new Programmer();
const instructions = desperateProgrammer
.code("A library in typescript within 10 days")
.cry("Oh God! Why everything is so complicated :'( ? Plz Help!!!")
// the next two lines shouldn't work here (test.js) as the user
// shouln't have asscess to the "retriveInstructionsMethod" and "secretKey"
// keys. I'm just showing it to demonstrate what I want to achieve.
[retriveInstructionsMethod](secretKey);
console.log(instructions);
Here I want to hide all the instructions given to a Programmer object. If I hide it from the end user then I won't have to validate those instructions before executing them later.
And the user of programmer.js module should be able to add new methods to a programmer. For example:
// addMethods from "methods.js" module
addMethods("debug", (...bugs) => {...});
Now I know that, I can create a base class and just extend it every time I add a new method. But as it is expected that there will be lots of external methods so soon it will become very tedious for the user.
Below is what I've tried so far. But the type annotation clearly doesn't work with the following setup and I know it should not! Because the Methods interface's index signature([key: string]: Function) is very generic and I don't know all the method's name and signature that will be added later.
methods.ts
export interface Methods {
[key: string]: Function;
}
const methods: Methods = {};
export default function getMethods(): Methods {
return { ...methods };
}
export function addMethods<T extends Function>(methodName: string, method: T) {
methods[methodName] = method;
}
programmer.ts
import type { Methods } from "./methods";
import getMethods from "./methods";
export type I_Programmer = Methods & {
addInstruction: (arg: { cmd: string; args: unknown[] }) => void;
};
interface ProgrammerConstructor {
new (): I_Programmer;
(): void;
}
const retriveInstructionsMethod = "SECRET_METHOD_NAME";
const secretKey = "ACCESS_KEY";
const Programmer = function (this: I_Programmer) {
const instructions: object[] = [];
this.addInstruction = (value) => instructions.push(value);
Object.defineProperty(this, retriveInstructionsMethod, {
enumerable: false,
writable: false,
value(key: string) {
if (key === secretKey) return instructions;
},
});
const methods = getMethods();
for (const key of Object.keys(methods))
this[key] = (...args: unknown[]) => methods[key].apply(this, args);
} as ProgrammerConstructor;
// this function is just to demonstrate how I want to extract all the
// instructions. It should not be accessible to the end user.
export function getInstructionsFrom(programmer: I_Programmer): object[] {
// gives error
// #ts-expect-error
return programmer[retriveInstructionsMethod][secretKey]();
}
export default Programmer;
testUsages.ts
import { addMethods } from "./methods";
import type { I_Programmer } from "./programmer";
import Programmer, { getInstructionsFrom } from "./programmer";
function code(this: I_Programmer, task: string, deadline: string) {
this.addInstruction({ cmd: "code", args: [task, deadline] });
return this;
}
function cry(this: I_Programmer, words: string) {
this.addInstruction({ cmd: "cry", args: [words] });
return this;
}
addMethods("code", code);
addMethods("cry", cry);
const desperateProgrammer = new Programmer()
.code("a library with typescript", "10 days") // no type annotation of "code" method
.cry("Oh God! Why everything is so complicated :'( ? Plz Help!!!"); // same here
// Just for demonstration. Should not be accessible to the end user!!!
console.log(getInstructionsFrom(desperateProgrammer));
Kindly give me some hint how I can solve this problem. Thanks in advance.
I have been using the class-validator decorator library to validate dto objects and would like to create a domain whitelist for the #IsEmail decorator. The library includes a #Contains decorator, which can be used for a single string (seed), but I would like to input an array of valid strings instead. Is this possible?
current
#Contains('#mydomain.com')
#IsEmail()
public email: string;
desired
#Contains(['#mydomain, #yourdomain, #wealldomain, #fordomain'])
#IsEmail()
public email: string;
If this is not possible, I would appreciate any direction on how to implement a custom decorator which serves this purpose.
Thanks.
Consider this example:
const Contains = <Domain extends `#${string}`, Domains extends Domain[]>(domains: [...Domains]) =>
(target: Object, propertyKey: string) => {
let value: string;
const getter = () => value
const setter = function (newVal: Domain) {
if (domains.includes(newVal)) {
value = newVal;
}
else {
throw Error(`Domain should be one of ${domains.join()}`)
}
};
Object.defineProperty(target, propertyKey, {
get: getter,
set: setter
});
}
class Greeter {
#Contains(['#mydomain', '#wealldomain'])
greeting: string;
constructor(message: string) {
this.greeting = message;
}
}
const ok = new Greeter('#mydomain'); // ok
const error = new Greeter('234'); // runtime error
Playground
If you provide invalid email domain it will trigger runtime error. It is up to you how you want to implement validation logic. It might be not necessary a runtime error
I have an application built on typescript with decorators for some convenience property assignments and wondering how I can go about writing unit tests for them.
export function APIUrl() {
return function (target: any, key: string) {
let _value = target[key];
function getter() {
return _value;
}
function setter(newValue) {
_value = getApiURL();
}
if (delete target[key]) {
Object.defineProperty(target, key, {
get: getter,
set: setter
});
}
};
}
In a spec class I have,
it("should return url string", ()=> {
#APIUrl();
let baseURL:string;
expect(baseURL typeOf string).toBe(true)
})
Since decorators are just functions I would suggest to just test them like any other function. And only if you really need to, add one tests that shows how to use the decorator with a class/member/...
Here is an example such a test could look like:
import test from 'ava';
import { APIUrl } from './path';
const decorate = new APIUrl();
test.before(t => {
let obj = { someProp: 'foo' };
decorate(obj, 'someProp');
t.context.foo = obj;
});
test('should return original value', t => {
t.is(t.context.foo.someProp, 'foo');
});
Another approach could be to setup some properties and/or methods that use your decorators and test their usage directly.
Note: decorators can only be used on class methods and members so you'd need to create a dummy class in your test.
Here's an example:
//Test Setup
class Test {
#APIUrl()
url: string;
#AnotherDecorator()
anotherFunction() {}
}
//Unit tests
describe('Decorator Tests', () => {
it('should work', () => {
const t = new Test();
expect(t.url).toEqual("something");
expect(t.anotherFunction()).toReturn("something else");
});
}