Appiumに基づくUI自動テスト

UI自動化テストが必要な理由

モバイルアプリは複雑なシステムであり、さまざまな機能間の結合が非常に強く、単体テストのみで全体的な機能を保証することは困難です。UIテストはモバイルアプリケーション開発の重要な部分ですが、実行速度は遅く、反復的なワークロードがたくさんあります。これらのワークロードを減らして作業効率を向上させるには、持続可能な統合された自動テストソリューションを導入する必要があります。

Appiumを選ぶ理由

Appiumは、Android / iOS / WindowsでネイティブアプリケーションとWebハイブリッドアプリケーションをテストするために使用できるオープンソースのテストツールです。

  1. モバイルアプリケーション機能の急速な反復に対処するために、ますます多くのアプリがハイブリッドモードを採用しています。つまり、一部の機能がアプリケーションの埋め込みWebページに渡されます。Appiumは、ネイティブアプリケーションまたはアプリに埋め込まれたWebページを簡単に切り替えてテストでき、ハイブリッドアプリを適切にサポートしています。

  2. Appiumは各プラットフォーム自体が提供するテストフレームワークを使用するため、サードパーティのコードを導入したり、アプリケーションを再パッケージしたりする必要はありません。

    プラットホーム テストフレームワーク
    Android 4.2以降 UiAutomator / UiAutomator2(デフォルト)
    Android 2.3以降 計装(Selendroid提供)
    iOS 9.3 以上 XCUITest
    iOS 9.3以下 UIAutomation
  3. AppiumはGitHubのオープンソースであり、頻繁に維持されており、コミュニティは比較的活発です。コミュニティの継続的な取り組みにより、Appiumは常に最新バージョンの携帯電話オペレーティングシステムと公式のテストフレームワークとの互換性を維持でき、その機能は基本的なログの収集、画面の記録、opencvベースの画像認識などを含め、ますます完全になりつつあります。バージョンによって追加されたiOS 13 / Android 10のサポート。

  4. Appiumは、カスタムプラグインによる要素の検索をサポートしています。人工知能に基づくアイコン認識制御のサンプルプロジェクトなど、GitHubで利用できるサードパーティのプラグインもあります。プラグインをカスタマイズして、画像認識、OCR、およびその他の方法を使用してページ要素を検索することもできます。

きゅうりを使用してケースを整理する

Appiumは、Java、Pythonなどのさまざまなプログラミング言語をサポートしていますが、ケースを維持するためにコードを直接使用することは可読性が低く、学習コストも比較的高くなります。Cucumberの導入により、ケースを自然言語に近い方法で整理できます。CucumberはBDD(Behaviour-Driven Development)をサポートするツールです。文法規則テンプレートをカスタマイズして、テキストで説明されている手順をコードによって実行される手順に変えることができます。CucumberとJava 8は中国語のテキストエンコーディングと互換性があるため、英語のコードよりも理解しやすい中国語の操作手順をカスタマイズできます。最も基本的なクリック操作の定義を例にとると、予想される文法規則は"当 点击 [元素名称]"であり、次の定義を使用できます。

   // Cucumber使用正则表达式匹配引号中的内容作为type参数
   @当("^点击 \"([^\"]*)\"$")
   public void findElementAndClick(String type) throws Throwable {
       // driver为Appium对待测设备的抽象,所有测试步骤最终转为对driver对操作
       // type可以传入元素ID对应的字符串,By.id表示通过元素resource-id查找
       driver.findElement(By.id(type)).click();
   }

ケースを作成するときは、UIオートメーションテストに一般的に使用されるページオブジェクトデザインパターンを使用します。つまり、ページで操作可能または検証可能な要素を含み、一般的なメソッドを追加する、APPでテストする必要があるUIページのページオブジェクトを定義します。

Huajiaoホームページを例にとると、「Search」、「My」、「Start Broadcast」などの要素に対応する検索メソッド(たとえば、検索要素に対応する検索ボタン)を含む「Homepage」という名前の新しいオブジェクトを作成できます。 resource-idはcom.huajiao:id/main_home_top_search)です。検索ページでユーザーuidを入力して検索するのが一般的な操作であるため、これに「検索」メソッドを定義できます。すべてのテストケース、Pageオブジェクト、要素、およびメソッドは、テストバックグラウンドWebページを使用して保存および編集され、基本的なキーワード補完機能が実現されます。

上記のように、クリック、スライド、テキスト入力などの基本的な操作を定義します。適切なページとメソッドを確立したら、ユースケースを自然に近い(#コメント行で始まる)ケースの説明に変換できます

# "$首页.搜索"表示使用"首页"Page中的"搜索"元素
当 点击 $首页.搜索
# "$搜索.搜索()"表示调用搜索页面的搜索方法,括号内为搜索关键词参数
$搜索.搜索(43011080)
当 断言元素出现 $搜索.搜索结果

複雑なカスタム操作を実行するコードを書く

Cucumberを使用して、クリック、スライド、テキストの検証などの一般的な操作を定義すると、テストケースの作成の作業負荷が軽減され、テストケースの可読性が向上しますが、すべての関数が一般的な操作を使用できるわけではありません。特に、Cucumberは命令の段階的な実行のみをサポートし、命令の分岐またはループはできないため、複雑な操作ロジックでは、操作を完了するためにカスタムステップでコードを記述する必要があります。コード部分のカプセル化は、Androidによって公式に提供されるEspressoプロジェクトを参照しており、「find-operate-verify」のプロセスはチェーンコールによって実行されます。

Androidクライアントのログアウトを例にとると、下部にある「Home-My」要素をクリックします。現在のステータスがログインしていない場合、ログインポップアップがポップアップします。このとき、下部にある「Home-My」要素は表示されておらず、ログインしていないことを示しています。状態。

Cucumberは順次実行され、実行できないため"我的"元素可见时退出登陆,不可见时关闭登陆弹窗、ログアウト手順をカスタマイズするコードを記述する必要があります。

    @当("^退出登录$")
    public void logout() throws Throwable {
        // 点击"首页-我的"
        onView(By.id("com.huajiao:id/bottom_tab_user")).perform(click());
        try {
            // 如果当前用户已登陆,不会弹窗提示登陆,"首页-我的"元素可见
            onView(By.id("com.huajiao:id/bottom_tab_user")).check(matches(isDisplayed()));
            // 调用退出登录的方法
            logOut();
        }
        // 未登录状态,"首页-我的"元素不存在,抛出NoSuchElementException
        catch (NoSuchElementException e) {
            // 点击系统back键关闭登陆弹窗
            onActions().pressKeyCode(AndroidKey.BACK, "1").perform();
        }
    }

Appiumを使用してUI要素を見つける

  1. 基本的な検索方法

    • By.id:要素のresource-idを検索します。
    • MobileBy.AndroidUIAutomator(String code):UIAutomator2のコードテキストで検索します。codeUIAutomator2仕様のコードテキストに準拠するために、Appiumはテキストを解析し、リフレクションを使用してUIAutomator2を呼び出して検索します。以下はUiSelector検索テキストに含まれるtext要素の使用方法ですString code = "new UiSelector().textContains(\"" + text + "\");";
  2. xpath find elements
    xpathを使用して、XMLドキュメント内の要素と属性を検索できます。エレメントを取得するためのAppiumおよびGoogleの公式のuiautomatorviewerツールはxml形式で編成されており、xpathは、特定できるが特定できないエレメントBy.id正確に特定できますBy.className

    • コピーは「TEXT」要素の兄弟要素であり、兄弟要素のリソースIDは「ID」です。
      xpath://*[@text='TEXT')]/../android.widget.TextView[@resource-id='ID']
    • resource-idは「ID」であり、選択された状態要素の子要素です。子要素のattr属性はvalueです。 xpath://*[@resource-id='ID' and @selected='true']/*[@attr='value']

    xpathメソッドは要素を見つける方がより正確ですが、要素のパスはレイアウトの変更によって影響を受ける可能性があり、iOSのパフォーマンスは良くありません。そのため、resource-id最初に位置決め要素を組み合わせるなどの方法を使用することをお勧めします

  3. 画像認識検索要素
    Appiumは、By BySelectorレベルでの画像による検索をサポートしていますBy by = MobileBy.image(base64ImageString)現在、複数要素の検索はサポートされていません。最初に見つかった要素のみが返されます。
    Appiumが画像検索をサポートするには、少しの準備作業が必要です。

    1. OpenCVライブラリのNodeJSバージョンをインストールします。npm install -g opencv4nodejs

    2. Appiumで関連パラメーターを構成します。

      // 设置图片识别阈值,默认0.4。需要尝试在找不到元素和找到不匹配元素间的平衡
      driver.setSetting(Setting.IMAGE_MATCH_THRESHOLD, 0.5);
      // 图片识别耗时较长,可以在操作元素对时候不再次查找图片,以节省时间
      driver.setSetting(Setting.CHECK_IMAGE_ELEMENT_STALENESS, false);
      

StaleElementReferenceException: Appiumが要素を見つけ、その要素を操作しようとすると、現在のページのDOMリソース上にその要素が存在しない場合、StaleElementReferenceException例外がスローされますAppiumは、UIAutomator2を使用して要素を検索するときに、要素のキャッシュを保持します。要素を操作するときは、キャッシュされた情報を直接クリック、スライド、その他の操作のためにUIAutomator2に渡します。

  • 実際のテストプロセスでは、ページAがページBにジャンプし、ページBの要素elをクリックするという手順があります。ページAとBの両方に、elと同じIDの要素があります。ページBの要素elを操作しようとすると、Appiumは直接表示されるページAのキャッシュを使用しStaleElementReferenceExceptionます。
  • Appiumは要素の検索と操作にHTTPリクエストを使用するため、要素の検索と操作の実際のプロセスは、POST検索要素->サーバーキャッシュ要素-> POST操作キャッシュ要素であり、時間間隔があります。また、ネットワークリクエスト中にAPP側のポップアップウィンドウなどの要素がブロックされた場合にも発生することがありますStaleElementReferenceException

全体的なワークフロー

  1. htestクライアントクライアントは、パッケージ化されたAndroidパッケージサーバーのダウンロードリストを取得し、そこから最新のAPKインストールパッケージバージョンをフィルタリングします。携帯電話の最新バージョンよりも高いバージョンがある場合、携帯電話のHuajiao APPが上書きされ、BVTテストケースの実行が自動的にトリガーされます(単一のケースを実行する場合、テストプラットフォームのWebページから直接トリガーされます)。
  2. テストプラットフォームは、Cucumberによって記述されたBVTユースケースセットを選択し、同時にページページを見つけて、ユースケースステップの要素とメソッドをエスケープし、クライアントが使用できる要素ロケーターに置き換えます(id:最初はリソースID text:を検索することを意味し、最初はテキストコンテンツを検索することを意味します) )、HTTPリクエストを介してクライアントに返されます(単一のケースの実行時にソケットを使用して送信します)。
  3. テストケースの実行中に、要素を検索するときに携帯電話のポップアップウィンドウがHuajiao APP要素を覆っている場合があります。そのため、テストケースの実行中に、携帯電話に表示され、テストステップでは予期されないポップアップウィンドウが検出されます。 、最初の課金ポップアップ、ギフトダウンロードポップアップを開くなど、ポップアップを閉じた後、要素をもう一度探します。
  4. htestクライアントはAppiumドライバーを初期化し、Appiumをプロキシとして使用して携帯電話に接続し、携帯電話のテストケースで基本的な操作を実行します。
  5. テストケースの実行に失敗した場合、失敗したケースの再実行を試みます。失敗した場合、携帯電話のログを収集し、スクリーンショットと画面の記録を保存し、失敗したログをテストプラットフォームに保存します。単一のケースを実行する場合は、ソケットを使用して実行結果を送信します、結果はhtestサーバーを介してテストプラットフォームに返されて表示されます。bvtの場合、結果データはインターフェースを介して返されます。

テストプラットフォームを使用して、ウェブ上で一度にテストケースを実行します。


ソフトウェアテスト、インターフェーステスト、自動テスト、インタビューの経験を交換する場合。興味がある場合は、ソフトウェアテスト通信(1085991341)を追加できます。同僚との技術交流があります。
モジュールで分割すると、フレームワーク全体が次のように分割されます。

  1. テストプラットフォーム:Webページ。Cucumberベースのテストケースの保存と編集、ページページの管理、ユースケースの要素の解析、エスケープされたユースケースのクライアントへの送信に使用され、クライアントの実際の実行結果を表示します。

  2. htestサーバー:Javaミドルウェア、使用されたnettyフレームワーク、ソケットメッセージの転送を担当します。つまり、テストプラットフォームはクライアントにユースケースメッセージの実行を通知し、クライアントの実行結果はテストプラットフォームに戻ります。使用する:

    • htestのサーバー側のnettyの開始com.htest.server.server.BaseServer

      @Overridepublic void run() {
          if (bossGroup == null) {
              bossGroup = new NioEventLoopGroup();
              model.setBossGroup(bossGroup);
          }
          if (workerGroup == null) {
              workerGroup = new NioEventLoopGroup();
              model.setWorkGroup(workerGroup);
          }
          ServerBootstrap b = new ServerBootstrap(); 
          b.group(model.getBossGroup(),model.getWorkGroup())
              .channel(NioServerSocketChannel.class)
              .option(ChannelOption.SO_BACKLOG, 100)
              .option(ChannelOption.SO_KEEPALIVE, true)
              .handler(new LoggingHandler(LogLevel.INFO))
              .childHandler(getChildHandler());
          try {
              future = b.bind(SERVER_IP, getPort()).sync(); 
              LOGGER.debug("服务启动成功 ip={},port={}",SERVER_IP, getPort());
              future.channel().closeFuture().sync();
          } catch (Exception e) {
              LOGGER.error("Exception{}", e);
          } finally {
              Runtime.getRuntime().addShutdownHook(new Thread() {
                  @Override public void run() {
                      shutdown();
                  }
              });
          }
      }
      
    • HttpServer、JarServer、およびWebsocketServerはすべて同じ方法で起動されますが、異なるポートでリッスンし、異なるデータハンドラーを処理する点が異なります。

    • HttpServerのプロセッサはcom.htest.server.handler.ServerHttpHandler、メッセージの処理がhttpプロトコルに従って処理されることです。

      @Override
      protected void messageReceived(ChannelHandlerContext ctx, HttpRequest request) {
          try {
              this.request = request; headers = request.headers();
              if (request.method() == HttpMethod.GET) {
                  QueryStringDecoder queryDecoder = new 
                      QueryStringDecoder(request.uri(), Charset.forName("utf-8")); 
                  Map<String, List<String>> uriAttributes = queryDecoder.parameters(); //此处仅打印请求参数(你可以根据业务需求自定义处理) 
                  for (Map.Entry<String, List<String>> attr : uriAttributes.entrySet()){
                      for (String attrVal : attr.getValue()) {
                          Logs.HTTP.debug(attr.getKey() + "=" + attrVal);
                      }
                  }
              }
              if (request.method() == HttpMethod.POST) {
                  fullRequest = (FullHttpRequest) request;
                  //根据不同的 Content_Type 处理 body 数据
                  dealWithContentType();
              }
              keepAlive = HttpHeaderUtil.isKeepAlive(request);
              writeResponse(ctx.channel(), HttpResponseStatus.OK, "开始执行", keepAlive);
          } catch (Exception e) {
              writeResponse(ctx.channel(), HttpResponseStatus.INTERNAL_SERVER_ERROR, "启动失败", true);
          }
      }
      
    • JarServerのプロセッサはcom.htest.server.handler.ServerHandler、メッセージの処理がprotobuf形式に従って処理されることです。

      @Override
      protected void handleData(ChannelHandlerContext ctx, MessageModel.Message msg) {
          Connection connection = server.getConnectionManager().get(ctx.channel());
          connection.updateLastReadTime();
          server.getMessageReceiver().onReceive(msg, connection);
      }
      
    • WebsocketServerのプロセッサはcom.htest.server.handler.ServerChannelHandler、protobuf形式に従ってメッセージも処理することです。HttpServerとの違いは、ChannelInitializerが異なることです。

  3. htestクライアント:Javaクライアント、Cucumberステップの定義、電話APKの更新、Appiumの初期化、およびテストケースの実行に使用されます。使用法:PC側のコマンドラインで実行しjava -jar htest-client.jarます。PC側には、yaml構成ファイルによって制御されるAppiumおよびnodejs opencv環境が必要です。テストの実行中にパラメーターを終了します。具体的な作業方法は次のとおりです。

    • 機能:jarは最新のapk関数の定期的なチェックをサポートします。これはデフォルトで無効になっており、yamlファイルの設定で有効になります。最新のapkが見つかると、携帯電話に自動的にインストールされ、Webサーバー(自動化されたケース管理のテストプラットフォーム)にリクエストが送信され、指定されたモジュールケースセットの実行がトリガーされます。
    • ダウンロード戦略:ローカルのyaml設定ファイルのapkVersion値がサーバーのapkVersion値より大きい場合、システムはデフォルトで最新のapkのみをダウンロードします。サーバーより小さい場合はダウンロードされません。
    • インストール戦略:ダウンロードが完了すると、電話のapkのversionName(aaptによって解析されます)がダウンロードされたapkのversionNameと比較されます。ダウンロードされたapkが新しい場合はインストールしてください。それ以外の場合はインストールされません。設定パラメータは、指定された携帯電話にインストールすることもできます。携帯電話が1台だけの場合、設定パラメータは不要です。
    • インストールが完了すると、apkVersion値は次の判断のために自動的に更新されます。
    • インストールが完了すると、WebサーバーにHTTPリクエストが送信されます。Webサーバーはそれを受信すると、それを一度トリガーして現在の携帯電話ケースセットタスクに送信します。特定のケースセットモジュールはモデルパラメータで構成され、メール受信者はメールで構成されます。
  4. Appium:UIAutomator2 / XCUITestを介して携帯電話に接続するために使用されるNodeJSクライアント/サーバーは、携帯電話での要素の取得/クリック/スワイプなどの基本的な操作を実行します。
    上記のコンテンツはこの記事のコンテンツ全体です。助けてくれた友達は好きでコメントすることができます。

おすすめ

転載: blog.csdn.net/Chaqian/article/details/106607198