ウェブサイト検索

TypeScript - 関数内で同期API呼び出しを行い、チェーン可能に返す方法は?


以下のコードで私が試そうとしていることはこれです。

  1. 関数areAllTypesFoundの中で3回API呼び出しを行います。それらのAPI呼び出しの結果を使って、必要なデータをすべて取得できたかどうかを判断します。

  2. もしコールから必要なすべてのデータを取得できたら、チェーン可能なものの中にラップされたブール値を返す。

  3. 関数doStuffの中で、私はareAllTypesFoundを呼び出します。その呼び出しが完了した後にのみ、その真偽値の結果を使って何かを行います。その後、さらにいくつかのコードを実行します。

問題は、すべてのデータが見つかっている場合でも、areAllTypesFound が常に false を返すことです。これは Cypress の非同期的な性質に関係していると思われます。次のコードですべてを同期的に実行する方法を誰か教えてくれませんか?

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 !
    }

}

itemType 関数がどのようなものかのサンプルコード -

  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",
      },
    });
  }

回答 #1

APIコールをネストすることができます。よりエレガントな方法があるかもしれませんが、コールをネストして各レベルで返すことで、最終的なリターンが行われる前に各コールが完了していることを保証できます。

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
    }
  }
}

allResultsには何でも入れることができますが、配列には3つの要素を入れる必要があります。


例

私は、再現可能な形で問題のコードを探索したかった。

役に立つウェブサイト requestly.com があります

ネットワークトラフィックの傍受と変更、セッションリプレイ、モックサーバー、APIクライアントによって、ウェブアプリをより速く構築、テスト、デバッグしましょう。

このサイトは、あなたの cy.request() 呼び出しに遅延(またはその他の変更)を加えることができ、これによりテストコードをストレステストして、コードが不安定でないことを確認することができます。

使い方は、遅延時間をミリ秒単位で、実際のAPIのURLのように埋め込むことです:

https://app.requestly.io/delay/<delay_in_millisecods>/<URL_to_be_delayed

私がテストしたいAPIはhttps://jsonplaceholder.typicode.com/todos/で、idが1、2、3のレコードをリクエストしています。

目標は、関数(または質問者の場合はクラスメソッドですが、同じ考え方です)からすべての3つの結果が返されることを保証することです。

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')
  })


ネストされた呼び出しではなく、順次の呼び出し

テストの結果、入れ子になった cy.request() の呼び出しは必要なく、cy.then() を使うことで関数にすべてのリクエストが完了するのを待たせることができることがわかりました(ちなみにこれは私の元々の即答の答えでした)。

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')
  })

だから、cy.then() は通常の Javascript の async/await のように振る舞います。

回答 #2

技術的には、問題となっているのは allTypesPresent で欲しい結果を 早期に評価する ことです。

cy.wrap(value-or-expression) は常に value-or-expression をできるだけ早く評価します。その評価はコマンドキューの設定中に発生し、コマンドが実行され始める前に行われます。

評価を遅延させる一つの方法は、式を関数でラップし、結果が欲しいとき(つまりreturn文の時)に関数を呼び出すことです。

あなたのコード const allTypesPresent=type1Present && type2Present && type3Present は、評価が早すぎています。

評価を延期するには、cy.wrap(() => type1Present && type2Present && type3Present) を使用できます。

次に、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
} 

便利のために、同じことを行う小さなカスタムコマンドを追加することができます

Cypress.Commands.add('evaluate', (fn) => fn())

static areAllTypesFound(params) : Cypress.Chainable<boolean> {

  ...
  return cy.evaluate(() => type1Present && type2Present && type3Present);
}

追伸 この解決策は他の回答にも別の形で存在しますが、特に cy.wrap() に関して、遅延評価のアイデアを強調したかったのです。


別名を使用する

通常、エイリアスは単一の値を持ちますが、カスタムコマンドで配列を作成することもできます

Cypress.Commands.add('saveToAlias', (aliasName, value) => {
  cy.get(`@${aliasName}`, {log:false})
    .then(values => values.concat(value))
    .as(aliasName)
})

利点は、API呼び出しがローカル変数から切り離されていることと、戻り値が必要ないことです。エイリアスはテストのどこからでもアクセスできます。

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 !
}

関連記事