What is the best way to have jest spied function store function arguments using deep copy rather than by reference?

Attempting to test a complex class that handles fetching, caching, geocoding, and returning a list of places. The code being tested showcases the following structure:

interface Place {
  name: string;
  address: string;
  longitude: number | null;
  latitude: number | null;
}

class Places {
  findAll(country: string): Place[] {
    let places = this.placesCache.get(country)
    if (places === null)
      places = this.placesExternalApiClient.fetchAll(country);
      // BREAKPOINT no.1 (before store() call)
      this.placesCache.store(country, places);
      // BREAKPOINT no.2 (after store() call)
    }
    
    for (const place of places) {
      // BREAKPOINT no.3 (before geocode() call)
      const geocodedAddress = this.geocoder.geocode(place.address);
      place.longitude = geocodedAddress.longitude;
      place.latitude = geocodedAddress.latitude; 
    }  

    return places;
  }
}

class PlacesExternalApiClient {
  fetchAll(country: string): Place[] {
    // makes a request to some external API server, parses results and returns them
  }
}

class PlacesCache {
  store(country: string, places: Place[]) {
    // stores country and places in database with a relation
  }

  get(country: string): Place[] | null {
    // if country is in database, returns all related places (possibly []),
    // if country is not in db, returns null
  }
}

interface GeocodedAddress {
  address: string;
  longitude: number;
  latitude: number;
}

class Geocoder {
  geocode(address: string): GeocodedAddress {
    // makes a request to some geocoding service like Google Geocoder,
    // and returns the best result.
  }
}

The test scenario is as follows:

mockedPlaces = [
  { name: "place no. 1", address: "Atlantis", longitude: null, latitude: null },
  { name: "place no. 2", address: "Mars base no. 3", longitude: null, latitude: null },
]

mockedPlacesExternalApiClient = {
  fetchAll: jest.fn().mockImplementation(() => structuredClone(mockedPlaces))
}

mockedGeocodedAddress = {
  address: "doesn't matter here",
  longitude: 1337,
  latitude: 7331,
}

mockedGeocoder = {
  geocode: jest.fn().mockImplementation(() => structuredClone(mockedGeocodedAddress))
}

describe('Places#findAll()', () => {
  it('should call cache#store() once when called two times', () => {
    const storeMethod = jest.spyOn(placesCache, 'store');
    
    places.findAll('abc');
    places.findAll('abc');

    expect(storeMethod).toHaveBeenCalledTimes(1);
    expect(storeMethod).toHaveBeenNthCalledWith(
      1,
      'abc',
      mockedPlaces, // ERROR: expected places have lng and lat null, null
                    //    but received places have lng and lat 1337, 7331
    );
  })
})

A detailed debugging session revealed inconsistencies during the testing process:

  • At BREAKPOINT no.1, the variable places contains coordinates set to null.
  • Upon reaching BREAKPOINT no.2:
    • places still reflects coordinates as null.
    • placesCache.store retains the expected data with both places having coordinates set to null.
  • At the initial encounter with BREAKPOINT no.3, there are no changes in the data or variables.
  • However, on the second occurrence of BREAKPOINT no.3, the coordinates for the first place in both places array and .mock.calls[0][1] array shift from null to 1337, 7331.

This issue causes inaccuracies in the test results due to the way spyOn records arguments without deep copying, resulting in unexpected behavior when objects are mutated. How can I ensure spyOn performs a deep clone of the arguments it receives? Alternatively, how can I refine the testing methodology without altering the original business logic implementation?

The primary objective is to confirm that store() was called with longitude and latitude values set to null, despite the current test producing false negatives.

Answer №1

Initially, I believe that ridiculing a specific method in the cache storage does not provide any value. The exact arguments passed and how it is cached are usually irrelevant. What should matter to us are the following scenarios:

  • Multiple calls to the fetch method with identical properties only trigger one real call, and each time returns the correct (cached) result
  • Changing any of the significant properties results in a separate call and the actual result being returned

If you still wish to proceed with your approach, why not consider using jest.fn along with a call to the original function?

let originalStoreMethod = placesCache.store;
let callsToStoreMethod = [];

beforeEach(() => {
  placesCache.store = jest.fn().mockImplementation((...args) => {
    callsToStoreMethod.push(structuredClone(args));
    return originalStoreMethod.apply(placesCache, args);
  })
});

afterEach(() => {
  placesCache.store = originalStoreMethod;
});

....
    expect(callsToStoreMethod).toHaveLength(1);
    expect(callsToStoreMethod[0]).toEqual(
      1,
      'abc',
      mockedPlaces
    );

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Retrieving the values from unspecified keys within an array of objects

Receiving a JSON from an external source with an unpredictable number of keys is the challenge. The structure typically appears as follows: data = [{ id: 1, testObject_1_color: "red", testObject_1_shape: "triangle", testObject_2_color: "blue", testObject_ ...

add the array element to the object

How can I extract values from a nested array and push them into a new single object? const Ll = [ { _id: 'milk', category: [ [ { name: 'Alfred', job: 'manager' }, { ...

Avatar index is not defined

When I try to upload an avatar using this code, it works perfectly. However, if I submit the form with an empty avatar field, I receive an error message stating "undefined index avatar." The PHP code is listed first, followed by the HTML code, and finally ...

CSS injection not working in Chrome extension content script

I'm attempting to add a CSS file to the PayPal homepage, but I'm encountering issues with it not working properly. Here is the manifest file I am using: { "manifest_version": 2, "name": "Paypal Addon", "version": "1.0", "descript ...

Implementing a comprehensive Node application with a fully stacked npm and package.json structure

If there's a repository with both backend (Node/Express) and frontend client, the structure might look like this: ├── build ├── config ├── coverage │ └── lcov-report ├── dist │ └── static ├── server ( ...

Monitoring transactions through interactive buttons that lead to external destinations

Tracking purchases on my website using a dynamic button that redirects to other sites is essential. See the code snippet below: <div class="buyproduct"> <a onclick="target='_blank'" href="<?php echo $this->product['from&apos ...

leveraging jQuery across an Angular 2 application as a global tool

I'm currently exploring ways to incorporate jQuery globally throughout my angular 2 application. The only comprehensive resource I've come across so far is this Stack Overflow answer. However, despite my efforts, I haven't been able to make ...

The function getContacts is unable to retrieve data from the web server

I am currently learning Node JS, Express JS, and Angular JS. I'm working on building a contact application. On the server side, I have successfully set up programming using Node JS, Express JS, and storing data in MONGO DB. Testing with Postman has sh ...

Can you explain the occurrence of a parse error within an update panel?

Internal Server Error. The Parser Encountered an Error Description: An error occurred during the parsing of a resource required to fulfill this request. Please check the specific parse error details below and make appropriate changes to your source file. ...

Exploring the Past: How the History API, Ajax Pages, and

I have a layout for my website that looks like this IMAGE I am experimenting with creating page transitions using ajax and the history API. CODE: history.pushState(null, null, "/members/" + dataLink + ".php" ); // update URL console. ...

What is the best way to retrieve an object from d3.data()?

My force directed graph contains a variable with all my link data. var linkData = d3.selectAll(".link").data(); In order to change the class of certain edges based on their values, I am using a for loop to iterate through the data and identify the desire ...

Looking to add the Ajax response data into a dropdown selection?

Looking to add select options dynamically, but unsure how to retrieve response values. I am able to fetch all the values in the response, but I struggle with appending them correctly in the select option. I believe the syntax is incorrect. success: funct ...

Using Sencha Touch for asynchronous HTTP request to Model-View-Controller

Ext.util.JSONP.request({ url: '/Home/GetMessagesMobile', callbackKey: 'callback', params: { lat: geoip_latitude(), lng: geoip_longitude(), ...

Vue.JS encounter a hiccup while attempting to store data in the local storage

My application is set up to fetch data from a rest api, and it consists of two key components: 1 - (main) Display the results 2 - (list) To display a list of selected items from the main results The feature I am trying to implement involves adding a save ...

Place a stationary element under another stationary element

I have a dilemma with two fixed elements on my website. One of them can toggle between display: block and display: none, while the other is always visible. I want both elements to stay at the top of the webpage without overlapping each other. I've co ...

Incorporate JSON data into a JavaScript search in CouchDB

I am currently working with a CouchDB database that contains approximately one million rows. My goal is to query for specific rows in the database using the keys provided in an external JSON file. The approach I am currently using to achieve this involves ...

how can I insert an asp textbox into a div using jquery?

I have an asp.net textbox (<asp:TextBox></asp:TextBox>), and I would like it so that when I click a button, the textbox is placed inside a div like this output (<div><asp:TextBox></asp:TextBox></div>). I tried using the ...

Tips for guaranteeing that a javascript single page application runs exclusively on a single browser tab

Currently, I am in the process of creating a SPA application using the Ember.js framework. My main goal is to ensure that there is only one instance of the application running on a single tab within the same domain. I find it helpful to think of this as s ...

Toggle the availability of links based on the presence of route parameters

My routing configuration appears as follows (for readability, I've omitted the second parameter in when()): app.config(function($routeProvider, $locationProvider){ // Prepare for html5 mode $locationProvider.hashPrefix('!'); $l ...

Despite the presence of data within the array, the React array returns empty when examined using the inspect element tool

Currently, I'm working on creating an array of objects in React. These objects consist of a name and a corresponding color, which is used to represent the pointer on Google Maps. For instance, one object would look like this: {name: "Ben", colour: "gr ...