cypress: [Cypress 6.0.0] Overriding interceptors doesn't work.

Current behavior

With Version <6.0.0, if you used c.route with a stub, you could then use it again with a new stub and it would take the second fixture for the second request.

With version >6.0.0 It doesn’t happen, the override doesn’t work with cy.intercept and it always returns the initial fixture.

cy.intercept(
  {
     method: 'GET',
     url: 'items',
  },
   { fixture: 'items/empty-list.json' }
);

// Some more testing of the app, and then fetching again -

cy.intercept(
  {
     method: 'GET',
     url: 'items',
  },
   { fixture: 'items/list-1.json' }
);

The use case is very simple. Imagine my app loads with 0 items, using a GET. Then, I do some stuff, do a POST, and then GET those items again. I want to stub it with [item], instead of the initial [].

So what happens is that it always return [], not [item] as it used to. I’m quite sure it’s not a problem with my code, used to work perfectly fine till today.

The first image is the initial intercept, you can see it gets 2 image The second image is what is shown for the second GET, the one that should’ve matched and return [item] image

Desired behavior

I want to be able to override interceptors

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Reactions: 185
  • Comments: 55 (13 by maintainers)

Commits related to this issue

Most upvoted comments

This issue blocks me to update to 6.0.0 version

Huge thanks for the Cypress team for all of their hard work on this and other new features. Complex feature changes and new APIs always have some rough edges. Always impressed with the quality of your work. 👏

We are currently looking into ways to make this possible with cy.intercept() while still maintaining the value of the other features introduced in cy.intercept(). There’s some investigation we need to do to ensure all use cases can be covered in a way that makes sense.

For the future, it would be useful to mention this critical breaking change in your CHANGELOG or migration guides. All of these resources imply that cy.intercept is pretty much a drop-in replacement for cy.route:

There is nothing indicating the issues mentioned in this thread. I recommend you adjust both articles above and mention this difference for others attempting to upgrade.

I was really excited to use the new intercept ability to be able to dynamically respond to requests with different data based on query params or other URL-specific data. But this omission is even worse than the original problem: it altogether prevents us from testing failure cases. For example: testing that the UI responds as expected to when a given request responds with a 200 vs when it responds with a 404. Is this not an expected use case? Is there an intended workaround I’m missing?

The code for this is done in cypress-io/cypress#14543, but has yet to be released. We’ll update this issue and reference the changelog when it’s released.

@flotwig Yes, but for me cy.intercept is an only reason to upgrade

@bhaeussermann Thanks for sharing your feedback! This is very helpful.

We have not ruled out implementing overriding interceptors. It’s just very complex for us to design a clear API for people to override interceptors, have all of the new middleware features of intercept, AND possibly deliver the ‘times’ feature (because we believe this may be valuable outside of just this overwriting scenario).

The intercept should work how people intuitively expect it to work in all of these situations. We don’t want to release something that is confusing. Also we want to make sure that we don’t release an API that we later have to instantly retract due to confusion or introduce unnecessary breaking features, etc.

The team is working on it. Even though there’s not work directly on this exact PR.

The solution involving the “times” property does not seem practical for complex tests. The “counting” aspect seems very brittle, since if one request happened to be called once more than expected by the application, it would immediately be out of sync with the test. This seems like it would be a nightmare to maintain.

It also would not work for my project unless I’d make some drastic changes to my testing code. In a testing file we typically define a function that sets a number of default routes, and then override certain routes in different tests / contexts for testing special cases. Consider the following fictitious code as an illustration:

describe('Manage employees', () => {
	context('General', () => {
		beforeEach(() => {
			stubLookupRoutesDefaults();
			cy.visit('myPageUrl');
		});
		
		// Some general tests
		...
	});
	
	// Test Seychelles-specific logic
	context('Client in Seychelles', () => {
		beforeEach(() => {
			stubLookupRoutesDefaults();
			// Override with currencies relevant for Seychelles.
			cy.intercept('GET', 'currencies', ...);
			// Override with with packages having Seychelles-like properties.
			cy.intercept('GET', 'packages', ...);
			cy.visit('myPageUrl');
		});
		
		// Some Seychelles-specific tests
		...
	});
	
	// Test logic specific to Mauritius clients
	context('Client in Mauritius', () => {
		beforeEach(() => {
			stubLookupRoutesDefaults();
			// Override with banks having Mauritius-like properties.
			cy.intercept('GET', 'banks', ...);
			cy.visit('myPageUrl');
		});
		
		// Some Mauritius-specific tests
		...
	});
});

function stubLookupRoutesDefaults() {
	// Stub about 10 lookup API routes.
	cy.intercept(...);
	cy.intercept(...);
	...
}

Being able to override certain routes is the simplest way of achieving the testing logic above. Anything else suggested in this thread would significantly impact the complexity of the testing code.

I don’t understand why allowing to override routes (eg. by matching routes from last backwards instead of forwards) is not a viable solution. Will someone please explain this to me?

This would seriously help us loads with our suite. Our example is a graphql single endpoint, and I want to change certain responses dependent on when we fire mutations. e.g. when we load a table, load with intiial mock data, after firing a successful create record mutation then responding with a new set of mock data to include that record. Structure wise within our tests and app I’m really happy with it, it just all relies on this being possible!

Hey everyone, i ran into the same issues and needed to intercept a route in a test multiple times with different responses, this is the current workaround:

const responses = [settingsInitial, settingsDone];
const url = '<some api endpoint>';
cy.intercept('GET', url, (req) => req.reply(responses.shift()))
  .as('loadUserSettings');

It is indeed a huge blocker. And what @samijaber said:

Inability to overwrite or clear an interceptor should be explicitly stated in the docs.

In my case, I am testing my application to re-authenticate after a failing request and then to retry that failed request with the new credentials. No way to do that if I cannot override or clear the previous interceptor.

Hey everyone, i ran into the same issues and needed to intercept a route in a test multiple times with different responses, this is the current workaround:

const responses = [settingsInitial, settingsDone];
const url = '<some api endpoint>';
cy.intercept('GET', url, (req) => req.reply(responses.shift()))
  .as('loadUserSettings');

Awesome suggestion! Never thought of that! I did the following to use fixtures and also override different status codes:

        const responses = []
        cy.fixture('/api/security/signup/confirm/422.json').then((fixture) => {
          // first response
          responses.push({ status: STATUS_422, body: fixture })
        })

        cy.fixture('/api/security/signup/confirm/200.json').then((fixture) => {
          // second response
          responses.push({ status: STATUS_200, body: fixture })
        })

        cy.intercept('PUT', '**/security/signup/confirm', (req) => {
          const { status, body } = responses.shift()
          req.reply(status, body)
        })

@jennifer-shehane as for the api design, perhaps being explicit rather than implicit, with like a cy.clearIntercept method could be considered?

@ar7casper would the solution in #14513 solve your use-case?

It would use the first fixture just once on the very first request. And it would use the second fixture once, which would happen on the second request from the app

cy.intercept(
  {
     method: 'GET',
     url: 'items',
     times: 1
  },
   { fixture: 'items/empty-list.json' }
);

// Some more testing of the app, and then fetching again -

cy.intercept(
  {
     method: 'GET',
     url: 'items',
     times: 1
  },
   { fixture: 'items/list-1.json' }
);

This would solve several use-cases for us and would be a really important update, not only for our not-so-simple upgrade to cypress 6+ but also for our current suite of tests in general. Having this implementation quickly would be rather significant for us.

+1

Once cy.intercept() is declared for some route, you can not override its behaviour. It is quite annoying and until fixed will block me to update since we usually mock a ‘200’ response, but we also need to test other ones.

Looking forward a pull request to fix it.

So what are we doing with this? Its been a while and I cannot be expected to create a separate context every time I want a different API response mocked on the same url. That’s not a valid solution given our test layouts.

I was hoping to use overriding as a test organization tactic. For example: having several interceptors in a beforeEach, and then having one interceptor override in the more specific test itself to change a single piece of data to test a specific thing. Thus reducing repetition by forcing every test to declare the same intercepts.

@manojhans The issue with cy.get() returning null was fixed in 6.2.0: https://github.com/cypress-io/cypress/issues/9306

We’ve been running into this problem as well - trying to return different stubs for the same request, so we can test different responses.

Our solution is to use Cypress.config() to set a variable in each different test, and then check for that config variable in cy.intercept.

In my-test.spec.ts:

it('should return response One', () => {
  Cypress.config('request_type', 'type_one');
  cy.visit('/some-page'); // This page makes a GET call to /some-api-route
  cy.wait('@getResponseOne');
});

it('should return response Two', () => {
  Cypress.config('request_type', 'type_two');
  cy.visit('/some-page'); // This page makes a GET call to /some-api-route
  cy.wait('@getResponseTwo');
});

And in our intercepts.js file:

cy.intercept(
  {
    url: '/some-api-route',
    method: 'GET',
  },
  (req) => {
    const requestType = Cypress.config('request_type');
    if (requestType === 'type_one') {
      req.alias = 'getResponseOne';
      req.reply({ fixture: 'response-one.json' });
    } else if (requestType === 'type_two') {
      req.alias = 'getResponseTwo';
      req.reply({ fixture: 'response-two.json' });
    }
  },
);

Values set by Cypress.config() in a spec file are only in scope for the current spec file, so you don’t need to worry about polluting the global config scope.

Hopefully there will be a way to clear/override intercepts introduced soon, but until then this method seems to work well for us!

We are currently looking into ways to make this possible with cy.intercept() while still maintaining the value of the other features introduced in cy.intercept(). There’s some investigation we need to do to ensure all use cases can be covered in a way that makes sense.

(11th December 2020)

At this point in time, are you able to say wether interceptors will eventually get overriding behaviour @jennifer-shehane?

I am asking because this issue is preventing us from rolling out some internal tooling using cy.intercept. If this is eventually going to be implemented, we can wait, but if not we should start building a workaround in our tooling.

@ar7casper would the solution in https://github.com/cypress-io/cypress/pull/14513 solve your use-case?

It would use the first fixture just once on the very first request. And it would use the second fixture once, which would happen on the second request from the app

cy.intercept(
  {
     method: 'GET',
     url: 'items',
     times: 1
  },
   { fixture: 'items/empty-list.json' }
);

// Some more testing of the app, and then fetching again -

cy.intercept(
  {
     method: 'GET',
     url: 'items',
     times: 1
  },
   { fixture: 'items/list-1.json' }
);

How about this quick monkey patch to solve the issue until an official solution is provided? Works for me with 6.6.0. Just put the following into your commands.js. Not sure about side effects but studying the code did not reveal any downsides of implementing a clearIntercept that way.

Assume you have defined your intercept as follows

cy.intercept('GET', '.netlify/functions/subjects/[0-9]*').as('GET-one-subject')

and calling it like that

cy.wait('@GET-one-subject')

then you can clear it with

cy.clearIntercept('GET-one-subject')

Here is the command

Cypress.Commands.add('clearIntercept', (aliasName) => {
  const routes = Cypress.state('routes')
  Object.entries(routes).forEach(([key, value]) => {
    const { alias } = value
    if (Object.is(alias, aliasName)) {
      delete routes[key]
    }
  })
  Cypress.state('routes', routes)
})

Update: This does not quite work as expected in all cases. Needs more investigation as more elements of the state seem to be touched.

I believe this is the crux of the issue https://github.com/cypress-io/cypress/blob/197f3097f7beaa09cd4dd1e2a0e9a39610f5ed69/packages/net-stubbing/lib/server/route-matching.ts#L110-L116

Assuming I understand everything correctly - it looks like the order by which the routes are being matched is the original order they’re pushed in.

Changing the code to this might solve it

 export function getRouteForRequest (routes: BackendRoute[], req: CypressIncomingRequest, prevRoute?: BackendRoute) { 
   const possibleRoutes = prevRoute ? routes.slice(_.findIndex(routes, prevRoute) + 1) : routes 
  
-   return _.find(possibleRoutes, (route) => { 
+   return _.find(possibleRoutes.slice().reverse(), (route) => { 
     return _doesRouteMatch(route.routeMatcher, req) 
   }) 
 } 

From what I could tell, previously it let the driver do this matching instead https://github.com/cypress-io/cypress/blob/197f3097f7beaa09cd4dd1e2a0e9a39610f5ed69/packages/driver/src/cypress/server.js#L268-L277

Unfortunately, I don’t have much time setting up a local environment to test & verify that it’s indeed the issue - if anyone wants to do it they’re more than welcome to use my code diff 😃

@FrancoisCamus indeed 😃 Feel free to take a look at the issue comment in #14543 and let me know if it will cover your needs.

@diggabyte, an implementation like cy.clearIntercept("@alias") is something my team has been desperately wanting since the beginning of our cypress adventure.

@pszalanski, in my case, it would be a definitive and sensical way to clear/reset the usage of an intercept.

I.e. “This intercept is no longer registered/valid, is no longer watching/intercepting/stubbing this path and is no longer saving and queuing the requests/responses that can be fetched by cy.wait("@alias")

It would explicitly do what times would do implicitly. Instead of saying, “After n uses, this intercept is no longer registered,” it would say, “Unregister this intercept now.”

Like the times implementation, this would solve several use-cases for my team. In contrast to times, however, cy.clearIntercept would be much easier for us to implement. cy.clearIntercept would also be, for our cases, preferred over simply allowing intercept overrides (like before). Although it would require extra work for us to implement (as opposed to simply allowing overrides like with route), it would be useful enough to us in other areas that it would be worth the effort to us.

To stress the point and to summarize, cy.clearIntercept would be gold for us. times would work too, but something like cy.clearIntercept would be the clear winner by far for my team.

@artem-3dhubs can you give us an example of the test and the override logic you want to do?

PS: you can always create more complex programmatic intercepts with overrides yourself since cy.intercept allows callbacks, like https://glebbahmutov.com/blog/cypress-intercept-problems/#no-overwriting-interceptors-again I feel like we are trying to create a nice syntax around it

Same for me. I am using automatically recorded responses to process all the network requests an app does during the test. This logic runs before all the test cases in the test suite and sets up the mocks.

I would like to override some of the specific routes for the particular test case and assert the recorded response. That means I want to discard the previous intercept and apply the new one while keeping the same recorded mock. The times option does not seem to help.

I guess I could implement some workaround that will always use the global intercept, but it would be way more straightforward if native API just supported overriding same way as route did.

Also the way I managed to process multiple mocked responses for the same route with the route API is by using undocumented now('route', ...) command that I invoked in route’s onRequest callback to shift to the next mocked response by effectively overriding the previous route.

I was really excited to see the new intercept API that supported possibility to pass mocks at the actual request time. But overriding is still crucial to me.

Let me know if I am missing something or you need some more details about my use case.

Just wanted to add that I am also blocked from updating due to this.

@Tintef What works for me is using a global variable and changing it from within the test (in a ‘then’ callback)

let fixture = 'default'
beforeEach(() => {
  cy.intercept('url', (req) => {
    req.reply((res) => {
       res.send({ fixture: fixture });
     });
  });
});

it('test', () => {
   // fixture is 'default'
  cy.get('button.submit').then(() => {
    fixture = 'other_fixture';
  });
  // fixture is 'other_fixture'
});

@phishy we were thinking about this and here is how it breaks down in your particular case

Let’s say you set up intercepts in beforeEach hook. Are they used in the hook itself? A common situation that the author of this issue is experiencing (and I had the same in todomvc tests) is you set up the intercepts and visit the site inside the hook. Let’s say start with empty data

beforeEach(() => {
  cy.intercept('/todos', { fixture: 'empty.json' })
  cy.visit('/')
})

it('starts with empty list', () => {
  cy.get('.todo').should('have.length', 0)
})

Great. Now let’s see you want to simulate an API error. What happens when the server says 404 to GET /todos

it('shows an error if todos do not load', () => {
  // using magical command
  cy.overrideIntercept('/todos', { forceNetworkError: true })
  cy.get('.loading-error').should('be.visible')
})

Well, the above test will NOT work. By the time you override the intercept, the beforeEach hook has already finished running completely, and the original intercept has returned its empty list. Well, maybe you could trick it, and use cy.reload inside the test

it('shows an error if todos do not load', () => {
  // using magical command
  cy.overrideIntercept('/todos', { forceNetworkError: true })
  cy.reload()
  cy.get('.loading-error').should('be.visible')
})

Ok, this should work. Or we could use the {times: 1} approach. On the initial visit we expect the /todos request, so let’s set it up

beforeEach(() => {
  cy.intercept('/todos', { times: 1, fixture: 'empty.json' })
  cy.visit('/')
})

it('starts with empty list', () => {
  cy.get('.todo').should('have.length', 0)
})

it('shows an error if todos do not load', () => {
  cy.intercept('/todos', { forceNetworkError: true })
  cy.reload()
  cy.get('.loading-error').should('be.visible')
})

Just one more observation: you can always avoid overriding intercepts in my opinion by setting up a new test “stack” of hooks. In our example, we can avoid overriding and times: 1 syntax by:

describe('tests start with empty list', () => {
  beforeEach(() => {
    cy.intercept('/todos', { fixture: 'empty.json' })
    cy.visit('/')
  })

  it('starts with empty list', () => {
    cy.get('.todo').should('have.length', 0)
  })
})

it('shows an error if todos do not load', () => {
  cy.intercept('/todos', { forceNetworkError: true })
  cy.visit()
  cy.get('.loading-error').should('be.visible')
})

So I am pretty optimistic about times: N being explicit and declarative way to solve this problem for most (but not all, we understand that there are more case you might want to test) use cases.

@flotwig You are right, but the cy.server() and cy.route() was marked as deprecated in Cypress 6.0.0. We then migrated to the intercept() method but now we are having some tests failing because is not overriding.

@jcdesousa shouldn’t block you from anything. cy.route is still available in 6.0.0

This is a blocker for us, we had to stop the migration due to this issue.