I want to write a test that asserts a given object does not have certain properties.
Say I have a function
function removeFooAndBar(input) {
delete input.foo;
delete input.bar;
return input;
}
Now I want to write a test:
describe('removeFooAndBar', () => {
it('removes properties `foo` and `bar`', () => {
const data = {
foo: 'Foo',
bar: 'Bar',
baz: 'Baz',
};
expect(removeFooAndBar(data))
.toEqual(expect.objectContaining({
baz: 'Baz', // what's left
foo: expect.not.exists() // pseudo
bar: undefined // this doesn't work, and not what I want
}));
});
});
What's the proper way to assert this?
Update after the discussion in the comments
You can use expect.not.objectContaining(). This approach works fine but has one unfortunate edge case: It matches when the property exists, but is undefined or null. To fix this you can explicitly add those values to be included in the check. You need the jest-extended package for the toBeOneOf() matcher.
expect({foo: undefined}).toEqual(expect.not.objectContaining(
{foo: expect.toBeOneOf([expect.anything(), undefined, null])}
));
An example with nested props that fails:
const reallyAnything = expect.toBeOneOf([expect.anything(), undefined, null]);
expect({foo: undefined, bar: {baz: undefined}}).toEqual(
expect.not.objectContaining(
{
foo: reallyAnything,
bar: {baz: reallyAnything},
}
)
);
Original answer
What I'd do is to explicitly check whether the object has a property named bar or foo.
delete data.foo;
delete data.bar;
delete data.nested.property;
expect(data).not.toHaveProperty('bar');
expect(data).not.toHaveProperty('foo');
expect(data.nested).not.toHaveProperty('property');
// or
expect(data).not.toHaveProperty('nested.property');
Or make this less repeating by looping over the properties that will be removed.
const toBeRemoved = ['foo', 'bar'];
toBeRemoved.forEach((prop) => {
delete data[prop];
expect(data).not.toHaveProperty(prop);
});
However, the loop approach isn't too great for possible nested objects.
I believe what you are looking for is expect.not.objectContaining()
expect(data).toEqual(expect.not.objectContaining({foo: 'Foo', bar: 'Bar'}));
expect.not.objectContaining(object) matches any received object that
does not recursively match the expected properties. That is, the
expected object is not a subset of the received object. Therefore, it
matches a received object which contains properties that are not in
the expected object. - Jest Documentation
This answer is a paraphrase of the accepted answer. It is added only because of this exact suggestion to the accepted answer was rejected.
You can explicitly check whether the object has a property named bar or foo.
delete data.foo;
delete data.bar;
expect(data).not.toHaveProperty('bar');
expect(data).not.toHaveProperty('foo');
For nested properties:
delete data.nested.property;
expect(data.nested).not.toHaveProperty('property');
// or
expect(data).not.toHaveProperty('nested.property');
Or make this less repeating by looping over the properties that will be removed.
const toBeRemoved = ['foo', 'bar', 'nested.property'];
toBeRemoved.forEach((prop) => {
expect(data).not.toHaveProperty(prop);
});
However, the loop approach isn't too great for possible nested objects. What you are looking for is expect.not.objectContaining().
expect({baz: 'some value'}).toEqual(expect.not.objectContaining(
{foo: expect.anything()}
));
This approach works fine but has one unfortunate edge case: It matches when the property exists, but is undefined or null:
expect({foo: undefined}).toEqual(expect.not.objectContaining(
{foo: expect.anything()}
));
would also match. To fix this you can explicitly add those values to be included in the check. You need the jest-extended package for the toBeOneOf() matcher.
expect({foo: undefined}).toEqual(expect.not.objectContaining(
{foo: expect.toBeOneOf([expect.anything(), undefined, null])}
));
An example with nested props that, expectedly, fails:
const reallyAnything = expect.toBeOneOf([expect.anything(), undefined, null]);
expect({foo: undefined, bar: {baz: undefined}}).toEqual(
expect.not.objectContaining(
{
foo: reallyAnything,
bar: {baz: reallyAnything},
}
)
);
can you check the result? example?
const result = removeFooAndBar(data)
expect(result.foo).toBeUndefined()
expect(result.bar).toBeUndefined()
you can check initially that the properties were there.
The other option is to extend the expect function: https://jestjs.io/docs/expect#expectextendmatchers
expect.extend({
withUndefinedKeys(received, keys) {
const pass = keys.every((k) => typeof received[k] === 'undefined')
if (pass) {
return {
pass: true,
}
}
return {
message: () => `expected all keys ${keys} to not be defined in ${received}`,
pass: false,
}
},
})
expect({ baz: 'Baz' }).withUndefinedKeys(['bar', 'foo'])
I'd just try because you know the data value to use it:
const data = {...};
const removed = {...data};
delete removed.foo;
delete removed.bar;
expect(removeFooAndBar(data)).toEqual(removed);
Edit 1: Because of Jest's expect.not, try something like:
const removed = removeFooAndBar(data);
expect(removed).not.toHaveProperty('foo');
expect(removed).not.toHaveProperty('bar');
expect(removed).toHaveProperty('baz');
Do not check object.foo === undefined as others suggest.
This will result to true if the object has the property foo set to undefined
eg.
const object = {
foo: undefined
}
Have you tried use the hasOwnProperty function?
this will give you the following results
const object = {foo: ''};
expect(Object.prototype.hasOwnProperty.call(object, 'foo')).toBe(true);
object.foo = undefined;
expect(Object.prototype.hasOwnProperty.call(object, 'foo')).toBe(true);
delete object.foo;
expect(Object.prototype.hasOwnProperty.call(object, 'foo')).toBe(false);
It is possible to check whether an object has selected fields (expect.objectContaining) and in a separate assertion whether it does not have selected fields (expect.not.objectContaining). However, it is not possible, by default, to check these two things in one assertion, at least I have not heard of it yet.
Goal: create a expect.missing matcher similar to standard expect.any or expect.anything which will check if the object does not have the selected field and can be used alongside matchers of existing fields.
My attempts to reach this goal are summarized below, maybe someone will find them useful or be able to improve upon them.
I point out that this is a proof of concept and it is possible that there are many errors and cases that I did not anticipate.
AsymmetricMatchers in their current form lack the ability to check their context, for example, when checking the expect.any condition for a in the object { a: expect.any(String), b: [] }, expect.any knows nothing about the existence of b, the object in which a is a field or even that the expected value is assigned to the key a. For this reason, it is not enough to create only expect.missing but also a custom version of expect.objectContaining, which will be able to provide the context for our expect.missing matcher.
expect.missing draft:
import { AsymmetricMatcher, expect } from 'expect'; // npm i expect
class Missing extends AsymmetricMatcher<void> {
asymmetricMatch(actual: unknown): boolean {
// By default, here we have access only to the actual value of the selected field
return !Object.hasOwn(/* TODO get parent object */, /* TODO get property name */);
}
toString(): string {
return 'Missing';
}
toAsymmetricMatcher(): string {
return this.toString(); // how the selected field will be marked in the diff view
}
}
Somehow the matcher above should be given context: object and property name. We will create a custom expect.objectContaining - let's call it expect.objectContainingOrNot:
class ObjectContainingOrNot extends AsymmetricMatcher<Record<string, unknown>> {
asymmetricMatch(actual: any): boolean {
const { equals } = this.getMatcherContext();
for (const [ property, expected ] of Object.entries(this.sample)) {
const received = actual[ property ];
if (expected instanceof Missing) {
Object.assign(expected, { property, propertyContext: actual });
} // TODO: this would be sufficient if we didn't care about nested values
if (!equals(received, expected)) {
return false;
}
}
return true;
}
toString(): string {
// borrowed from .objectContaining for sake of nice diff printing
return 'ObjectContaining';
}
override getExpectedType(): string {
return 'object';
}
}
Register new matchers to the expect:
expect.missing = () => new Missing();
expect.objectContainingOrNot = (sample: Record<string, unknown>) =>
new ObjectContainingOrNot(sample);
declare module 'expect' {
interface AsymmetricMatchers {
missing(): void;
objectContainingOrNot(expected: Record<string, unknown>): void;
}
}
Full complete code:
import { AsymmetricMatcher, expect } from 'expect'; // npm i expect
class Missing extends AsymmetricMatcher<void> {
property?: string;
propertyContext?: object;
asymmetricMatch(_actual: unknown): boolean {
if (!this.property || !this.propertyContext) {
throw new Error(
'.missing() expects to be used only' +
' inside .objectContainingOrNot(...)'
);
}
return !Object.hasOwn(this.propertyContext, this.property);
}
toString(): string {
return 'Missing';
}
toAsymmetricMatcher(): string {
return this.toString();
}
}
class ObjectContainingOrNot extends AsymmetricMatcher<Record<string, unknown>> {
asymmetricMatch(actual: any): boolean {
const { equals } = this.getMatcherContext();
for (const [ property, expected ] of Object.entries(this.sample)) {
const received = actual[ property ];
assignPropertyCtx(actual, property, expected);
if (!equals(received, expected)) {
return false;
}
}
return true;
}
toString(): string {
return 'ObjectContaining';
}
override getExpectedType(): string {
return 'object';
}
}
// Ugly but is able to assign context for nested `expect.missing`s
function assignPropertyCtx(ctx: any, key: PropertyKey, value: unknown): unknown {
if (value instanceof Missing) {
return Object.assign(value, { property: key, propertyContext: ctx });
}
const newCtx = ctx?.[ key ];
if (Array.isArray(value)) {
return value.forEach((e, i) => assignPropertyCtx(newCtx, i, e));
}
if (value && (typeof value === 'object')) {
return Object.entries(value)
.forEach(([ k, v ]) => assignPropertyCtx(newCtx, k, v));
}
}
expect.objectContainingOrNot = (sample: Record<string, unknown>) =>
new ObjectContainingOrNot(sample);
expect.missing = () => new Missing();
declare module 'expect' {
interface AsymmetricMatchers {
objectContainingOrNot(expected: Record<string, unknown>): void;
missing(): void;
}
}
Usage examples:
expect({ baz: 'Baz' }).toEqual(expect.objectContainingOrNot({
baz: expect.stringMatching(/^baz$/i),
foo: expect.missing(),
})); // pass
expect({ baz: 'Baz', foo: undefined }).toEqual(expect.objectContainingOrNot({
baz: 'Baz',
foo: expect.missing(),
})); // fail
// works with nested!
expect({ arr: [ { id: '1' }, { no: '2' } ] }).toEqual(expect.objectContainingOrNot({
arr: [ { id: '1' }, { no: expect.any(String), id: expect.missing() } ],
})); // pass
When we assume that the field is also missing when it equals undefined ({ a: undefined } => a is missing) then the need to pass the context to expect.missing disappears and the above code can be simplified to:
import { AsymmetricMatcher, expect } from 'expect';
class ObjectContainingOrNot extends AsymmetricMatcher<Record<string, unknown>> {
asymmetricMatch(actual: any): boolean {
const { equals } = this.getMatcherContext();
for (const [ property, expected ] of Object.entries(this.sample)) {
const received = actual[ property ];
if (!equals(received, expected)) {
return false;
}
}
return true;
}
toString(): string {
return `ObjectContaining`;
}
override getExpectedType(): string {
return 'object';
}
}
expect.extend({
missing(actual: unknown) {
// However, it still requires to be used only inside
// expect.objectContainingOrNot.
// expect.objectContaining checks if the objects being compared
// have matching property names which happens before the value
// of those properties reaches this matcher
return {
pass: actual === undefined,
message: () => 'It seems to me that in the' +
' case of this matcher this message is never used',
};
},
});
expect.objectContainingOrNot = (sample: Record<string, unknown>) =>
new ObjectContainingOrNot(sample);
declare module 'expect' {
interface AsymmetricMatchers {
missing(): void;
objectContainingOrNot(expected: Record<string, unknown>): void;
}
}
// With these assumptions, assertion below passes
expect({ baz: 'Baz', foo: undefined }).toEqual(expect.objectContainingOrNot({
baz: 'Baz',
foo: expect.missing(),
}));
It was fun, have a nice day!
I would just try:
expect(removeFooAndBar(data))
.toEqual({
baz: 'Baz'
})
Related
I'm learning TypeScript, and decided to try implement it in a small portion of my codebase to get the ball rolling. Specifically, what I'm refactoring now is related to a fixture "factory" for the purpose of generating fixtures for Jest tests.
In addition to these factories, which spit out certain Objects, I also have some helper methods that make things like generating multiple objects a bit easier.
A factory is fairly simple, it looks something like this (the values are spoofed with faker.js):
function channelFactory(): ChannelItem {
return { foo: "bar" }
}
A ChannelItem is just a simple Object containing some keys
interface ChannelItem { foo: string; }
And as an example of one of those helper methods, I have a createMany function that takes in a Factory function and a Count as arguments
function createMany(factory: () => Record<string, unknown>, count = 1): Record<string, any>[] {
// A for loop that calls the factory, pushes those into an array and returns that array
}
However, if I try to use these factories somewhere, for example in this function that persists some created channels into the DB, I get the TS compiler warning me about Record<string, any>[] not being assignable to ChannelItem[].
function saveChannels(payload: ChannelItem[]): void { // Unimportant implementation details }
const items = createMany(channelFactory, 5);
saveChannels(items) // => Argument type Record<string, any>[] is not assignable to parameter type ChannelItem[] Type Record<string, any> is not assignable to type ChannelItem
I know this is a commonly known issue with Interfaces specifically (Issue #15300) and that the potential solution would be to declare a type rather than an interface, however in this situation I still get the same warning.
type ChannelItem = { foo: string } // Still gives me the above warning
What would the ideal way of making my factory functions more generic here be?
You could make the createMany function generic:
function createMany<K extends string, T>(factory: () => Record<K, T>, count = 1): Record<K, T>[] {
const arr = [];
for (let i = 0; i < count; i++) {
arr.push(factory());
}
return arr;
}
const items = createMany(channelFactory, 5);
console.log(items);
// Prints:
//[
// { foo: 'bar' },
// { foo: 'bar' },
// { foo: 'bar' },
// { foo: 'bar' },
// { foo: 'bar' }
//]
I made K extends string because you specified you want your record to have string keys. T can be anything you want.
Just have to fill in the functions yourself, not sure what you want done in those.
The createMany doesn't even need to know the type factory returns.
You can make it generic for more flexibility.
interface ChannelItem { foo: string; }
function channelFactory(): ChannelItem {
return { foo: "bar" }
}
function createMany<T>(factory: () => T, count = 1): T[] {
// A for loop that calls the factory, pushes those into an array and returns that array
return []
}
function saveChannels(payload: ChannelItem[]): void { }
const items = createMany(channelFactory, 5);
saveChannels(items)
TS playground
I want the typescript compiler to throw an 'Object is possibly 'undefined'' error when trying to directly access any element of the array if the array is not pre-checked for emptiness, so that you always have to check that element for undefined, for example, using an optional chaining
If it is pre-checked that the array is not empty, then you need to be able to access its elements as usual, without the need to check its elements for undefined
I need this in order to be sure that the array is not empty, so if it is empty, then access to any of its elements will immediately return undefined then chaining will not continue and there will be no possible errors like cannot read property of undefined
How do i do this?
Code example, maybe it will make my question clearer
interface Element {
a: {
aa: string;
bb: string;
};
b: {
aa: string;
bb: string;
};
}
const element: Element = {
a: { aa: "aa", bb: "bb" },
b: { aa: "aa", bb: "bb" },
};
type ElementArray = Element[];
const array: ElementArray = [element, element];
const emptyArray: ElementArray = [];
const getFirstAWithoutLengthCheck = (array: ElementArray) => {
return array[0].a; // i want the typescript compiler to throw an 'Object is possibly 'undefined'' error here
};
const getFirstAWithLengthCheck = (array: ElementArray) => {
if (array.length) {
return array[0].a; // shouldn't be any errors
}
return null;
};
const getFirstAOptChaining = (array: ElementArray) => {
return array[0]?.a; // shouldn't be any errors
};
// will throw error cannot read property a of undefined, so we need to use
// optional chaining or length check in this function, but typesript is not requiring it
console.log(getFirstAWithoutLengthCheck(array)); // aa
console.log(getFirstAWithoutLengthCheck(emptyArray)); // crash!
// checking array length, access to first element should work as usual, no errors
console.log(getFirstAWithLengthCheck(array)); // aa
console.log(getFirstAWithLengthCheck(emptyArray)); // null
// optional chaining, no errors
console.log(getFirstAOptChaining(array)); // aa
console.log(getFirstAOptChaining(emptyArray)); // undefined
As commented by #Roberto Zvjerković, you need to use noUncheckedIndexedAccess compiler flag.
Playground (The link has noUncheckedIndexedAccess flag turned on).
However, you need to use instead of if (array.length), you need to do if (array[0]). It is because, even if the length is non-zero, it does not ensure that the elements are "non-undefined". If the array were array = [undefined], it should have given runtime error. It is irrespective of the type of array if it can contain undefined or not.
I managed to make TypeScript throw an error in the first example and not throw an error in the third one but it sadly gives you the undefined error in the second example. Hope this helps you:
interface Element {
a: {
aa: string;
bb: string;
};
b: {
aa: string;
bb: string;
};
}
const element: Element = {
a: { aa: "aa", bb: "bb" },
b: { aa: "aa", bb: "bb" },
};
type ElementArray = [] | [Element] | [Element, ...Element[]];
const array: ElementArray = [element, element];
const emptyArray: ElementArray = [];
const getFirstAWithoutLengthCheck = (array: ElementArray) => {
return array[0].a; // i want the typescript compiler to throw an 'Object is possibly 'undefined'' error here
};
const getFirstAWithLengthCheck = (array: ElementArray) => {
if (array.length) {
return array[0].a; // shouldn't be any errors
}
return null;
};
const getFirstAOptChaining = (array: ElementArray) => {
return array[0]?.a; // shouldn't be any errors
};
// will throw error cannot read property a of undefined, so we need to use
// optional chaining or length check in this function, but typesript is not requiring it
console.log(getFirstAWithoutLengthCheck(array)); // aa
console.log(getFirstAWithoutLengthCheck(emptyArray)); // crash!
// checking array length, access to first element should work as usual, no errors
console.log(getFirstAWithLengthCheck(array)); // aa
console.log(getFirstAWithLengthCheck(emptyArray)); // null
// optional chaining, no errors
console.log(getFirstAOptChaining(array)); // aa
console.log(getFirstAOptChaining(emptyArray)); // undefined
TypeScript playground Ignore the error on element, TypeScript playground thinks that element is a DOM element
Challenge
Below is a simplified example of how we control and pass data in an app. It is used in many places and works to translate data between UI, APIs, and a database.
API and UI use camelCase.
Database uses snake_case.
Currently, it's an awkward combination of Partial/Pick types to get some typing where...
const item = { fooBar: 'something' }
Item.cast(item).value // returns type Partial<ItemModel>
Item.create(item).value // returns type ItemModel
The goal is to return the real returned object type.
// Examples
const item = { fooBar: 'something' }
Item.cast(item).value // returns { fooBar: 'something' }
Item.cast(item).databaseFormat // returns { foo_bar: 'something' }
Item.create(item).value // returns { id: '{uuid}', fooBar: 'something' }
Item.create(item).databaseFormat // returns { id: '{uuid}', foo_bar: 'something' }
const itemFromDatabase = { id: '{uuid}', foo_bar: 'something', baz: null }
Item.cast(itemFromDatabase).value // returns { id: '{uuid}', fooBar: 'something', baz: null }
Item.cast(itemFromDatabase).databaseFormat // returns { id: '{uuid}', foo_bar: 'something', baz: null }
Any ideas on this? I would image it's something like the Object.entries() return type but I can't figure out that right T keyof combination.
// https://mariusschulz.com/blog/keyof-and-lookup-types-in-typescript
interface ObjectConstructor {
// ...
entries<T extends { [key: string]: any }, K extends keyof T>(o: T): [keyof T, T[K]][];
// ...
}
Code
import camelcaseKeys from 'camelcase-keys'
import snakecaseKeys from 'snakecase-keys'
type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>
interface ItemModel {
id: string
fooBar: any
baz?: number
}
interface ItemDatabaseModel {
id: string
foo_bar: any
baz?: number
}
export class Item {
private _data: Partial<ItemModel>
public static cast(item: Partial<ItemModel | ItemDatabaseModel>): Item {
return new this(camelcaseKeys(item))
}
public static create(item: Optional<ItemModel | ItemDatabaseModel, 'id'>): Item {
// Use item.id or add item.id === null to force key
return new this(camelcaseKeys(item))
}
private constructor(input: Partial<ItemModel>) {
// Validate "input" properties have a Item class property setter, else throw
// foreach => this[key] = input[key]
}
get databaseFormat() { return snakecaseKeys(this._data) }
get value() { return this._data }
set id(value: string | null) {
// automatically generate an ID if null, otherwise validate
this._data.id = value
}
set fooBar(value: any) {
// validate
this._data.fooBar = value
}
set baz(value: number | null) {
// validate
this._data.baz = value
}
}
The best way you could achieve this would be to have some tool which goes through your typescript files looking to type/interface definitions and automatically outputs additional types with the converted keys based on what it finds.
At the moment typescript doesn't support this kind of conversion automatically via a type definition and I'd guess that they'd be quite cautious about adding something like that; concatenating string literals in type definitions is something which has yet to make it into the language, for instance, and what you're looking for here is quite a bit more complex than that, unfortunately.
Here is an object with several different key and value, and each props of value differ from each other, how to best describe this object using TypeScript? Especially the setValue method, how to limit type of the creatureType, prop and value?
const object = {
john: {
name: '',
age: 18
},
alien: {
height: 20,
power:100,
},
setValue(creatureType) {
const self = this
return function (prop) {
return function (value) {
self[creatureType][prop] = value
}
}
}
}
Your setValue() method will need to be generic if you want it to place strong restrictions on which properties and values go with which, uh, "creature type". Because the type of the object's setValue() method will be dependent on the type of the other properties of object, the compiler will give up trying to infer types for it; it's too circular for something that isn't a class. Either you could manually annotate all the types, which would be annoying, or you could split object into two pieces, say plainObject holding just the data, and then merge in the setValue() method which will be dependent on the type of plainObject, like this:
const plainObject = {
john: { name: '', age: 18 },
alien: { height: 20, power: 100 }
}
type PlainObject = typeof plainObject;
const object = {
...plainObject,
setValue<K extends keyof PlainObject>(creatureType: K) {
const self: PlainObject = this;
return function <P extends keyof PlainObject[K]>(prop: P) {
return function (value: PlainObject[K][P]) {
self[creatureType][prop] = value;
}
}
}
}
And you can verify that the compiler behaves as you want:
object.setValue("john")("age")(19); // okay
object.setValue("alien")("height")("pretty tall"); // error!
// "pretty tall" isn't numeric --> ~~~~~~~~~~~~~
object.setValue("john")("power")(9000); // error!
// "power" is wrong --> ~~~~~~~
object.setValue("elaine")("name")("elaine"); // error!
// "elaine"? -> ~~~~~~~~
Okay, hope that helps; good luck!
Link to code in Playground
Let's say I have an object:
const a = {
foo: 123,
bar: 'example'
}
This object is a part of many other objects i.e.
const b = {
a: a,
anotherField: "example"
}
Actually, I'm using TypeScript and all these objects are of the same class which I believe isn't important.
After serializing the b object to JSON I need to get this string (i.e. I just get the foo field from a):
{ a: 123, anotherField: "example" }
What is the easiest and most elegant way to tell JSON.stringify() how to convert the a object to a string?
Probably something similar to what Python allows.
You could define toJSON in a.
If an object being stringified has a property named toJSON whose value is a function, then the toJSON() method customizes JSON stringification behavior: instead of the object being serialized, the value returned by the toJSON() method when called will be serialized.
(source: MDN)
For example:
class A {
constructor(foo, bar) {
this.foo = foo;
this.bar = bar;
}
toJSON() {
return this.foo;
}
}
const a = new A(123, "some name");
const b = {
a: a,
anotherField: "example"
};
console.log(JSON.stringify(b)); // "{"a":123,"anotherField":"example"}"
You could use the replacer while stringifying:
const result = JSON.stringify(b, (k, v) => v && v.stringify() || v);
That way you can easily add a custom stringification to a:
const a = {
foo: 123,
bar: 'example',
stringify() { return this.foo; }
}