typescript - Hvordan lage synkrone API-kall og returnere kjedbare i en funksjon?
Her er hva jeg prøver å gjøre i koden nedenfor.
Foreta tre API-kall inne i funksjonen
areAllTypesFound. Bruk resultatene fra disse API-kallene til å avgjøre om jeg har fått all nødvendig data.Hvis jeg får all nødvendig data fra samtalene, returner da en boolsk verdi pakket inn i en kjedbar.
I funksjonen
doStuffgjør jeg et kall tilareAllTypesFound. Bare etter at det kallet er fullført, vil jeg gjøre noe med den boolske verdien. Etter det vil jeg utføre litt mer kode.
Problemet er at areAllTypesFound alltid returnerer false selv når alle dataene er funnet. Jeg er ganske sikker på at dette har noe å gjøre med den asynkrone naturen til Cypress. Kan noen vise meg hvordan jeg kan utføre alt synkront i koden nedenfor?
class MyClass {
static areAllTypesFound(params) : Cypress.Chainable<boolean> {
let idsType1 : number[];
let idsType2 :number[];
let idsType3 :number[];
let [type1Present, type2Present, type3Present] = [false, false, false];
// Here response = Cypress.Response<ItemType1 2 or 3>
ApiCaller.getItemType1(param1).then((response) => {
idsType1 = response.body.items.map((item) => item.id);
type1Present = idsType1.length > 0;
}
ApiCaller.getItemType2(param2).then((response) => {
idsType2 = response.body.items.map((item) => item.id);
type2Present = idsType2.length > 0;
}
ApiCaller.getItemType3(param3).then((response) => {
idsType3 = response.body.items.map((item) => item.id);
type3Present = idsType3.length > 0;
}
const allTypesPresent = type1Present && type2Present && type3Present;
// optional - maybe print all the "idsType" arrays.
return cy.wrap(allTypesPresent);
}
static doStuff() : void {
this.areAllTypesFound(params).then((areFoundBoolean) => {
if(areFoundBoolean) {
// do something
}else {
// do something else
}
});
// do more stuff only AFTER the above code is completed !
}
}
Eksempelkode for hvordan itemType-funksjonen ser ut -
public static getItemType1<T = ItemType1>(param1: string): Cypress.Chainable<Cypress.Response<T>> {
return cy.request({
method: "GET",
url: "/api/items/itemType1",
qs: {
itemType: param1,
},
headers: {
accept: "application/json",
},
});
}
Svar #1
Du kan nøste API-oppkallene. Det kan finnes en mer elegant måte, men ved å nøste oppkallene og returnere på hvert nivå sikrer du at hvert oppkall er fullført før den endelige returverdien gis.
const allResults = [] // gather the results in an array
return ApiCaller.getItemType1(param1).then((response) => {
idsType1 = response.body.items.map((item) => item.id);
allResults.push(idsType1)
return ApiCaller.getItemType2(param2).then((response) => {
idsType2 = response.body.items.map((item) => item.id);
allResults.push(idsType2)
return ApiCaller.getItemType3(param3).then((response) => {
idsType2 = response.body.items.map((item) => item.id);
allResults.push(idsType2)
return allResults
}
}
}
Du kan legge hva som helst i allResults, så lenge du får 3 ting i arrayen.
Et eksempel
Jeg ønsket å utforske koden det er snakk om i en reproduserbar form.
Det finnes et nyttig nettsted requestly.com
Bygg, test og feilsøk webappene dine raskere ved å avskjære og endre nettverkstrafikk, øktgjenopptak, mock-server og API-klient.
Dette nettstedet kan legge til forsinkelser (eller andre endringer) i dine cy.request()-kall, noe som lar deg stress-teste testkoden din for å sikre at den ikke er ustabil.
Måten du bruker det på er å legge inn forsinkelsestiden i millisekunder og den faktiske API-URL-en slik:
https://app.requestly.io/delay/<delay_in_millisecods>/<URL_to_be_delayed
API-en jeg vil teste er https://jsonplaceholder.typicode.com/todos/, og jeg ber om poster med id-ene 1, 2 og 3.
Målet er å sikre at alle tre resultatene returneres fra funksjonen (eller i OP er det en klassemetode, samme idé).
function apiCalls() {
const myApiBase = 'https://jsonplaceholder.typicode.com/todos/'
const urlWithDelay = 'https://app.requestly.io/delay/1000/' + myApiBase
return cy.request(urlWithDelay + '1') // record id 1
.then(response1 => {
return cy.request(urlWithDelay + '2') // record id 2
.then(response2 => {
return cy.request(urlWithDelay + '3') // record id 3
.then(response3 => {
return [response1.body.title, response2.body.title, response3.body.title]
})
})
})
}
apiCalls().then(console.log)
.should(results => {
expect(results[0]).to.eq('delectus aut autem')
expect(results[1]).to.eq('quis ut nam facilis et officia qui')
expect(results[2]).to.eq('fugiat veniam minus')
})
Sekvensielle (i stedet for nestede) kall
Det viser seg, etter testing, at du ikke trenger nestede cy.request()-kall, du kan tvinge funksjonen til å vente på alle forespørsler ved å bruke cy.then() (som forresten var mitt opprinnelige spontane svar).
function apiCalls() {
let api1
let api2
let api3
const myApiBase = 'https://jsonplaceholder.typicode.com/todos/'
const urlWithDelay = 'https://app.requestly.io/delay/1000/' + myApiBase
cy.request(urlWithDelay + '1')
.then(response => { api1 = response.body.title })
cy.request(urlWithDelay + '2')
.then(response => { api2 = response.body.title })
cy.request(urlWithDelay + '3')
.then(response => { api3 = response.body.title })
return cy.then(() => [api1, api2, api3])
}
apiCalls()
.should(results => {
expect(results[0]).to.eq('delectus aut autem')
expect(results[1]).to.eq('quis ut nam facilis et officia qui')
expect(results[2]).to.eq('fugiat veniam minus')
})
Så cy.then() oppfører seg som async/await i vanlig Javascript.
Svar #2
Teknisk sett er det som er galt den tidlige evalueringen av resultatet du ønsker i allTypesPresent.
cy.wrap(value-or-expression) evaluerer alltid value-or-expression så raskt som mulig, og det skjer under oppsettet av kommandokøen, og før kommandoene har startet å kjøre.
En måte å utsette evaluering på er å pakke uttrykket inn i en funksjon, og kalle funksjonen når du ønsker resultatet (dvs. ved return-setningen).
I koden din const allTypesPresent=type1Present && type2Present && type3Present evalueres for tidlig.
Du kan bruke cy.wrap(() => type1Present && type2Present && type3Present) for å utsette evalueringen.
Du kaller deretter funksjonen på punktet return.
static areAllTypesFound(params) : Cypress.Chainable<boolean> {
let idsType1 : number[];
let idsType2 :number[];
let idsType3 :number[];
let [type1Present, type2Present, type3Present] = [false, false, false];
// Here response = Cypress.Response<ItemType1 2 or 3>
ApiCaller.getItemType1(param1).then((response) => {
idsType1 = response.body.items.map((item) => item.id);
type1Present = idsType1.length > 0;
}
ApiCaller.getItemType2(param2).then((response) => {
idsType2 = response.body.items.map((item) => item.id);
type2Present = idsType2.length > 0;
}
ApiCaller.getItemType3(param3).then((response) => {
idsType3 = response.body.items.map((item) => item.id);
type3Present = idsType3.length > 0;
}
return cy.wrap(() => type1Present && type2Present && type3Present)
.then(fn => fn()); // evaluate
}
For bekvemmelighets skyld kan du legge til en liten tilpasset kommando for å gjøre det samme
Cypress.Commands.add('evaluate', (fn) => fn())
static areAllTypesFound(params) : Cypress.Chainable<boolean> {
...
return cy.evaluate(() => type1Present && type2Present && type3Present);
}
P.S Denne løsningen finnes også i andre former i de andre svarene, men jeg ønsket å fremheve ideen om utsatt evaluering, spesielt med hensyn til cy.wrap().
Bruk av alias
Vanligvis holder et alias en enkelt verdi, men vi kan bygge et array med en egendefinert kommando
Cypress.Commands.add('saveToAlias', (aliasName, value) => {
cy.get(`@${aliasName}`, {log:false})
.then(values => values.concat(value))
.as(aliasName)
})
Fordelen er at API-kallene er løst koblet fra lokale variabler, og returverdien er ikke nødvendig - aliaset kan aksesseres hvor som helst i testen.
static areAllTypesFound(params) : Cypress.Chainable<boolean> {
cy.wrap([]).as('results') // initialize
// Common response handler, all API calls
function checkResponse(response) {
const idsTypes = response.body.items.map((item) => item.id);
cy.saveToAlias('results', idsTypes.length > 0)
}
ApiCaller.getItemType1(param1).then(checkResponse)
ApiCaller.getItemType2(param2).then(checkResponse)
ApiCaller.getItemType3(param3).then(checkResponse)
}
static doStuff() : void {
this.areAllTypesFound(params)
cy.get('@results').then(results => {
const allTypesPresent = results.every(result => result) // all true?
if(allTypesPresent) {
// do something
}else {
// do something else
}
});
// do more stuff only AFTER the above code is completed !
}
