segmentio / analytics-next

Segment Analytics.js 2.0
https://segment.com/docs/connections/sources/catalog/libraries/website/javascript
MIT License
403 stars 136 forks source link

How to mock in e2e testing #1017

Closed brianabaker closed 9 months ago

brianabaker commented 9 months ago

Hello! We're moving off of Segment analytics loaded in our window to this analytics-next library.

For Analytics testing, we do mostly RTL unit testing & also e2e testing with Cypress (e2e only for certain flows). I'm running into some issues modifying our Cypress mocks.

I'm not sure how to mock Segment to consider itself "initialized" like we used to when the code was on our window. I can see the mocked analytics when my code first loads (via console logs), but then they are overwritten (I think when Segment initializes).

This is how we used to mock, is there something i can do to replicate setting window.analytics.initialize to true?

export const stubAnalytics = (win) => {
  win.analytics = win.analytics || [];

  // stops the segment script tag from executing, which would overwrite out stubs
  win.analytics.initialize = true;

  cy.stub(analytics, "track").as("analytics.track");
};

Thanks!

silesky commented 9 months ago

@brianabaker

How do you initialize segment: can you share that code?

brianabaker commented 9 months ago

For sure. Some code is removed from here, but it's the general idea: @silesky

// Our own Segment Tracker file -- missing a lot of the code but this is enough to get a feel I think

import { AnalyticsBrowser } from "@segment/analytics-next";

export const analytics = new AnalyticsBrowser();

export class SegmentService implements Service {
  track = (event: Event): Event => {

    analytics.track(name, underscorify(getProperties(event)));
    return event;
  };
}

// this gets called in another file 
export const getInstance = (): Tracker => {
  // `instance` here is an instance of SegmentService, our own class above!
  if (!instance) {
    const segmentWriteKey = process.env.KEY;
    const allowAnalytics = window?.doNotTrack !== true;

    analytics
      .load(
        { writeKey: `${segmentWriteKey}` },
          // i added disable: true here temporarily to try and help mock, it didn't assist.
        { integrations: { All: allowAnalytics }, disable: true },
      )
      .catch((e: Error) => console.error("Error loading Segment", e));
    instance = new SegmentService();
  }

  return instance;
};
silesky commented 9 months ago

@brianabaker It sounds like disable is not what you're after, since you're actually attempting to assert that analytics are being sent, correct?

From a testing pyramid perspective, I would generally recommend that you stick to using disable: true with e2e tests and test Segment via mocking the SegmentService in your regular unit tests, rather than attempting to e2e test a library that you don't 'own'. Since you're not using global analytics, there's no easy way to just stub out analytics -- and if something is not easy to mock, IMO it is a sign that something is off with either my app architecture or testing design/ strategy.

If you are determined to test our library in Cypress, I would recommend using Cypress HTTP request interception if you need that level of e2e-'ness' -- our Playwright example (see route.fulfill) should get you started on which endpoints you need to intercept. Like all HTTP Request Interception libs/code, it will probably require some trial and error.

https://github.com/segmentio/analytics-next/blob/master/packages/browser-integration-tests/src/index.test.ts

brianabaker commented 9 months ago

@silesky Okay, thank you! ~Interestingly I still saw Segment API calls with disable: true. Those should have not taken place in my tests, yeah?~ I figured out why they were firing, thanks!