OpenAPITools / OpenAPITools/openapi-generator
[REQ] Typesafe cypress intercept / wait calls
Nobody has claimed this yet.
- Dominant language
- Java
- Stars
- 26.8k
- Forks
- 7.7k
- PR merge metrics
- PR metrics pending
Description
Is your feature request related to a problem? Please describe.
I'm always not exactly frustrated, more like annoyed, when I have to deal with plain JSON objects where a typesafe solution is theoretically possible. I'm going to over-simplify things a bit now, so apologies in advance.
Let's assume now the typescript-angular generator and the sample pet store openapi specification. We get the PetService class, which has a nice method eg. for getting the pets by id:
public getPetById(petId: number): Observable<Pet>;
It knows that it will have to make a GET call to the /pet/${petId} endpoint, and that petId is a number. It also knows that the response will be a single Pet object.
Similarly, we know how to update those pets:
public updatePet(body: Pet): Observable<any>;
Here we know it's a PUT call to the /pet endpoint, and we know that the request body is going to be a Pet object.
And then we go to Cypress, and when we want to mock the GET endpoint, we go with
cy.intercept(
{ method: 'GET', url: 'http://localhost:8080/pet/555' },
{ statusCode: 200, body: { id: 99, name: 'Killer', categories: [ "great dane", "male" ] } }
// now we could just define a const body : Pet = { ... } whatever here to fix the immediate problem, but nothing enforces that.
// and programmers are lazy to look up what the right type is in the first place
)
Now a couple problems with the above:
- we must manually set the method, even though it can be extracted from the openapi specification
- we must manually set the url, even though it can be extracted from the openapi specification
- we must manually set the statusCode, even though for a big chunk of e2e tests it can be defaulted (to 200 in this case)
- we must manually create the body, which is just a typeless json, and we are missing a required field (photoUrls), and we have a nonexistent field (categories), and nothing tells us about that, even though the type can be extracted from the openapi specification
Let us move on to updating an existing pet. In this case the interception will look like this:
cy.intercept(
{ method: 'PUT', url: 'http://localhost:8080/pet' },
{ statusCode: 200 }
).as('update-pet-5643');
The problems with this are the same as before. And of course we will want to verify that the request we made here actually corresponds to the data that we have typed on the form, so we continue with:
cy.wait('@update-pet-5643').then(interception => {
const body = interception.request.body;
expect(body.id).to.equal(5643);
expect(body.name).to.equal('Killer');
})
The new problem is that the request body is again just a json, not a Pet, so we have to make guesses either about its actual type, or about the fields it has available.
Describe the solution you'd like
So having the openapi specification, I was thinking of generating something along the lines of:
export class petServiceExpect {
static getPetById(
petId: number, /* this is part of the url, so it's required */
body: Pet | ErrorResponse | null, /* we need a response body, which is either a Pet or a generic error or nothing at all */
statusCode = 200 /* in some cases it will be 400 or 404 or 401, but most of the time it will probably be 200 */) {
// this is the exact same code we had before, but now it is generated in a typesafe manner
cy.intercept(
{
method: 'GET', // method is obtained from the openapi specification
url: `http://localhost:8080/pet/${petId}` // url as well, plus we are injecting in the pet id, same as in the pet service
},
{ statusCode, body /* Body is type-safe, we know it is a Pet, and a valid one. If it's not valid, the test will fail. */ }
);
};
private static updatePetCounter = 0;
static updatePet(body : Pet, statusCode = 200): string {
const alias = `petService-updatePet-${this.updatePetCounter++}`; // here we generate a unique alias for this call
cy.intercept(
{ method: 'PUT', url: `http://localhost:8080/pet` },
{ statusCode, body }
).as(alias);
return alias; // and the alias will be returned for a later use
};
// This is magic for request validation
static updatePetVerifyRequest(
alias: string, // this is the alias we generated in the previous step
callback: (input: Pet) => void) // and this is a callback that will receive the type-safe request body
: void {
cy.wait(`@${alias}`).then(interception => {
// here we pass the request body, as a Pet, to the callback, which now will be able to to validate its fields
callback(<Pet>interception.request.body);
});
};
private static updatePetWithFormCounter = 0;
// of course, this is not all, eg. we would also need to handle optional and required query params
static updatePetWithForm(petId: number, name: string | null, statusCode = 201) {
let queryParams = '';
// this looks unnecessarily complicated, but if you assume 4-5 optional query params,
// and don't know which one will be set and which one won't, then you need to set all of them
// in a bit of a roundabout way
if (name != null) {
if (queryParams === '') { queryParams = '?'; } else { queryParams += '&'; }
queryParams += 'name=' + name;
}
const alias = `petService-updatePetWithForm-${this.updatePetWithFormCounter++}`;
cy.intercept(
{
method: 'POST',
url: `http://localhost:8080/pet/${petId}${queryParams}`
},
{ statusCode }
).as(alias);
return alias;
}
}
So lots of generated code, and now comes the magic. So in the angular components, we will do something like
petService.getPetById(55).then( pet => { ... } );
And when we want to mock the call above, we do this:
petServiceExpect.getPetById(55, { id: 55, name: 'Killer', photoUrls: ["url1", "url2", "url3"] });
And the body will be correct, because the parameter is a Pet, so if we don't define all the fields, or if the interface changes, the test will fail.
As for the update, we will have:
const alias = petServiceExpect.updatePet(55);
petServiceExpect.updatePetVerifyRequest(alias, pet => { // pet here is again typesafe
expect(pet.name).to.equal('Killer');
expect(pet.photoUrls[0]).to.equal('url95');
});
Describe alternatives you've considered
What I'm doing right now is I post-process the generated service files in gradle, kinda recreate the info from the original openapi specification and based on that put together these classes, which works fine, but the generation code is still a mess. The main issue is that the typescript-angular generator does not expose all the info I would need for naming and stuff, and mustache does not offer the string manipulation functions I would need to force it to work properly.
Additional context
I guess the first question is, is this something sane that people would use? Or is this something totally pointless on the JS/TS side, and it's only me who thinks it's useful because of my java background? I wouldn't want to put too much effort into this if noone would want to use it.
Then, I am kind of misusing the generated code, so I don't really know how to properly put together a project around this. Plus I don't know how js/ts packages/modules/whatevers work in general. I would definitely need some help for that. I think I should be able to figure out the mustache and java parts.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by reviewing the typescript-angular generator, its Mustache templates, and the generated service files described in the issue, along with the existing Gradle post-processing approach. Define the helper API and how OpenAPI operation, parameter, response, and request-body metadata would reach generated TypeScript code; done requires an agreed design and corresponding generator coverage for Cypress intercept and wait calls.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- angular, cypress, java, openapi, typescript
- Domain
- developer-experience, testing-qa, tooling
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 25/100