前文
私は、数年のモバイルアプリケーションの堅実な歴史を持ち、それに応じてかなり堅実で巨大なコードを備えた、十分に大きく、言葉を恐れないかさばるモバイルアプリケーションを作っている会社で働いています。
顧客からの願いの流れは多様で豊富であり、これに関連して、例えば、これを意図していない場所にも変更を加える必要があります。 これにより発生する問題のいくつか-回帰バグ-は、かなり複雑な時間を時々提供します。
同時に、何らかの理由で、プロジェクトの手動テストとかなり印象的な数のテスターのみが存在し、それを自動化するためのかなり素朴な試みは、「Hello world」レベルのいくつかのやや些細な単体テストのレベルにとどまりました。
特に、テスト部門には回帰を検索するための印象的なテストサイクルがあります。これは非常に定期的に実行され、かなりの時間がかかります。 したがって、タスクが発生すると、何らかの形でこのプロセスを最適化します。 これについて説明します。
正直なところ、私が見た自動受け入れテストのツールと、それらが私に合わなかった理由を覚えていません。 (コメントの誰かがこれを解決するための興味深いオプションを教えてくれれば非常に感謝します-私はおそらく非常に価値のあるものを見逃しています)私たちのアプリケーション、実際にはシンクライアントは多くのケースを持っていないので(まあ、少なくともユニットテストでカバーする方法はわかりませんが、何か他のものが必要です。 何らかの方法で、受け入れテストを自動化するライブラリを作成することが決定されました。
システム自体とその使用について
そのため、このシステムは次の要件を満たしている必要があります。
- システムは、アプリケーションランタイムでテストを実行できる必要があります。
- このシステムは、テスターがテストを人間の言語から自動的に実行できるものに翻訳できるようにする必要があります。
- システムは、ある意味でユーザーが見るデータへのアクセスを提供するという意味で受け入れテストをカバーする必要があります。
- システムはポータブルでなければなりません。 (iOS以外のプラットフォーム、場合によっては他のプロジェクトに適応できることが望ましい)
- システムは、テストを外部ソースと同期できるようにする必要があります。
- システムは、テスト結果を外部サービスに送信できるようにする必要があります。
大ブロックのテストシステムは、次のブロックで構成する必要があります。
潜在的なテスターは、次のスキームに従って行動する必要があります。
- webGUIにアクセスし、ログインしていくつかのテストケースとテスト計画を作成/編集します
- デバイスでアプリケーションを起動し、テストインターフェイスを開きます(たとえば、アプリケーションの任意の時点で3本の指でタップする)
- サーバーから目的のテスト計画とテストケースを取得する
- テストを実行します。 それらの一部は正常に完了し、一部は落下し、一部は(たとえば、UIテスト)追加の検証が必要になります
- webGUIでこの実行履歴を開き、追加の検証が必要なテストを見つけ、追加のデータ(たとえば、その時点でのスクリーンショット)に基づいて、テストが成功したかどうかを自分で書き留めます
この記事では、TestCoreモジュールについて詳しく説明します。 おおよそ次のスキームで説明できます。
アクションのシーケンス:
- ネイティブマクロは読み取られ、キーを使用してマクロテーブルに配置されます-キーを使用して、テストケースから呼び出すことができます
- テストケースのファイルが読み込まれます-構文解析ツリーの構築に基づいて、レクサーとパーサーを介して実行されます。
- テストケースは、キー-名前でテストケースのテーブルに配置されます。
- 外部からチームがテスト計画を実行し、ユーザーにテスト結果が表示されます。
そして、この瞬間からさらに詳しく:
テストケースは、次の形式の構造です。
.
テストアクションは、マクロ呼び出し、算術、または割り当てです。 たとえば、システムの正確性を検証するために使用した最も単純な例の一部を次に示します。
#simpleTest /*simple comment*/ send("someSignal") waitFor("response")->timeOut(3.0)->failSignals("signalError")->onFail("log fail") #end
#someTest paramA,paramB log("we have #paramA and #paramB") #end
#mathTest foo = 1 + 2 bar = foo * 3 failOn(bar == 9, "calculation is failed") failOn(((1 + 2) < 5),"1 + 2 < 5 : false") failOn(NOT((1 + 2) > 5), "1 + 2 > 5 : true") failOn(NOT("abc" == "def"), "true equality of abc and def") failOn("abc" == "abc", "false equality of abc and abc") failOn(1 == 2, "compeletly wrong equality") #end
send、log、waitFor、およびその他の演算子はマクロです(上記で説明しました)。これらは実際にはネイティブメソッドであり、その記述はテスターではなくプログラマーの肩にかかっています。
ここで、たとえば、ロギングマクロコード:
@implementation LogMacros -(id)executeWithParams:(NSArray *)params success:(BOOL *)success { NSLog(@"TESTLOG: %@",[params firstObject]); return nil; } +(NSString *)nameString { return @"log"; } @end
そして、ここでは、マクロコードFailOn-本質的にアサートです。
@implementation FailOnMacros -(id)executeWithParams:(NSArray *)params success:(BOOL *)success { id assertion = [params firstObject]; NSString *message = nil; if (params.count > 1) message = params[1]; if ([assertion isKindOfClass:[NSNumber class]]) { if ([assertion intValue] == 0) { *success = NO; TCLog(@"FAILED: %@",message); } } return nil; } +(NSString*)nameString { return @"failOn"; } @end
したがって、多数のカスタムマクロ(上記の例および他のいくつかのマクロ)を記述することで、アプリケーションデータへのアクセスを提供し、これらのテストで検証し、UIアクションを実行し、スクリーンショットをサーバーに送信できます。
重要なマクロの1つは、アプリケーションがそのアクションに応答することを期待するwaitForマクロです。 彼は、現時点では、アプリケーションコードがテストケースの実行に影響を与える主要なポイントの1つです。 つまり、ライブラリが快適に動作するためには、プロジェクトに固有の特定の数のマクロを記述するだけでなく、状態の変化、要求の送信、応答の受信などに関するさまざまなステータス信号をアプリケーションコードに導入する必要もあります。 つまり、言い換えると、この方法でテストされるようにアプリケーションを準備します。
ボンネットの下
そして、フードの下で、楽しみが始まります。 メイン部分(レクサー、パーサー、エグゼキューター、実行ツリー)はC、YACC、およびLexで記述されています-このようにコンパイル、実行、およびiOSだけでなくCが可能な他のシステムでもテストを正常に解釈できます。興味-ネイティブではないiOS言語とお気に入りのIDE XCodeの統合の複雑さに関する別の記事を書きたいと思います-すべての開発はその中で行われましたが、この記事では多かれ少なかれコードについてのみ説明します。
レックス
既に理解しているように、この問題を解決するために、小さくても非常に誇り高い解釈スクリプト言語が作成されました。つまり、解釈のタスクが最大限に発揮されます。 自作の自転車の高品質な解釈のために、それほど多くの資金はなく、私はYACCとLEXの束を使用しました。
Habrtの主題に関するいくつかの記事がありました(正直なところ、それらはスタートには十分ではありませんでした。ある種の複雑すぎないが、あまり明白ではない使用例は本当に欠けていました。このようなタスクが発生します-私のコードは、ある種の開始を支援します):
コンパイラーの記述に関する一連の記事。コンパイラーの仕組みに関する洞察とともに 。
1つの簡単な例に関する短い記事 。
レクサーに関するWiki 。
YACCに関するWiki 。
さて、他の多くの便利なリンクと...
簡単に言うと、Lexerのタスクは、パーサーが文字ごとに提供されるのではなく、既に定義されたタイプを持つトークンの準備済みシーケンスを提供することです。
長いリストで記事を散らかさないようにするために、レクサーの1つのコードを次に示します。
レクサー
実際、算術記号、数字、名前を区別し、入力としてパーサーに渡します。
ヤック
基本的に、YACCは、かつて書かれたバッカスナウア形式の言語の記述を言語インタープリターに変換する魔法のようなものです。
主なパーサーコードは次のとおりです。
パーサー
理解するために、その一部を検討してください。
program: alias module END_TERMINAL {finalizeTestCase($2);} ; module: /*this is not good, but it can be nil*/ {$$ = NULL;} | expr new_line {$$ = listWithParam($1);} | module expr new_line {$$ = addNodeToList($1,$2);} ;
YACCは構文ツリーを生成します。つまり、実際には、テストケース全体が1つのプログラムノードに折りたたまれます。1つのプログラムノードは、ケース宣言、アクションリスト、および最終ターミナルで構成されます。 次に、アクションのリストは、関数呼び出し、算術式などから折りたたむことができます。 例:
func_call: NAME_TOKEN '(' param_list ')' {$$ = functionCall($3,$1);} | '(' func_call ')' {$$ = $2;} | func_call '->' func_call {$$ = decorateCodeNodeWithCodeNode($1,$3);} ; param_list: /*May be null param list */ {$$ = NULL;} | math {$$ = listWithParam($1);} | param_list ',' math {$$ = addNodeToList($1,$3);} ; math: param {$$ = $1;} | '(' math ')' {$$ = $2;} | math sign math {$$ = mathCall($2,$1,$3);} | NOT '(' math ')' {$$ = mathCall(signNOT,$3, NULL);} | MINUS math {$$ = mathCall(signMINUS,$2, NULL);} ;
特に、たとえば、関数呼び出しはその名前、パラメーター、修飾子です。
一般に、YACC自体はトークンノードを単純に通過し、それらを折りたたみます。 これで何かをするために、論理は、ある種の構文構造に従って各パスでハングアップします。これは、将来使用できるメモリ内に正確にツリーを作成します。 理解するには-YACC表記で
$$は、この式に関連付けられた結果です
$ 1、$ 2、$ 3 ...は、これらの表現の対応する音素に関連付けられた結果です。
また、listWithParam、mathCallなどの呼び出しに対して、メモリ内のノードを生成して接続します。
ノード
ノードの生成方法のソースは、ここで読むことができます:
ノード生成ロジック
ノード見出し
実際、ノードは、それらが一緒に表すグラフをバイパスし、ツアーの結果に基づいて、テストに関する何らかの結論を得るために必要です。 実際、それらは抽象構文木でなければなりません
プログラム式を$$に折り畳んだ後、このツリーだけが得られ、計算できます。
執行者
エグゼキューターコードは次の場所に保存されています。
実際、左から右への深さのツリーの再帰的な構文解析です。 ノードは、そのタイプに応じて、mathNode(算術)またはoperatingNode(マクロを呼び出し、パラメーターのリストをコンパイル)として解析されます。
このグラフのリーフは定数(文字列、数値)、または変数名のいずれかであり、初期解析段階でルックアップテーブルを形成し、それらのメモリセルへのクイックアクセスインデックスを取得し、計算段階でこれらのセルにアクセスするか、マクロを呼び出します同様に、ブリッジモジュールを介して、この名前とパラメーターのリストを使用してマクロの実行を要求します(この時点でパラメーターが既に計算されていることを忘れないでください)。 また、他にも多くのルーチンがあり、メモリ管理、データ構造などに関連する瞬間はあまりありません。
呼び出し例
結論として、この奇跡が実際にネイティブコードからどのように呼び出されるかの例を示します。
-(void) doWork { TestReader *reader = [[TestReader alloc] init]; [reader processTestCaseFromFile:[[NSBundle mainBundle] pathForResource:@"testTestSuite" ofType:@"tc"]]; [reader processTestHierarchyFromFile:[[NSBundle mainBundle] pathForResource:@"example" ofType:@"th"]]; [self performSelectorInBackground:@selector(run) withObject:nil]; } -(void) run { [[[TestManager sharedManager] hierarchyForName:@"TestFlow"] run]; }
テスト計画の実行はすべて、メインスレッドではなく、別のスレッドで実行されます。アプリケーションデータへのアクセスを必要とするマクロを作成する場合、これを忘れないことをお勧めします。 Runメソッドの出力には、TestHierarchyクラスのオブジェクトがあります。このオブジェクトには、名前と実行ステータスを含むTestCaseクラスのオブジェクトのツリーと、もちろんログ内のブナの束が含まれています。
PSとして
奇妙なことに、テスターはこのことを喜んで受け入れ、現在、このことは徐々にプロジェクトへの実装の準備をしています。 後でこの素晴らしいプロセスについて書くのは素晴らしいことです。
iOS用のTestCoreモジュールのソースコードは、githubのリンクにあります: github.com/trifonov-ivan/testSuite
一般に、仕事のかなりの部分はむしろ独学のために行われましたが、同時に何らかの論理的な結論に至りました-したがって、アイデアの弱点を教えていただければ非常に感謝します-より効果的に解決する方法このタスク。 あなたはどう思いますか-本格的なテストサービスにアイデアを発展させる価値はありますか?
ええ、はい-部品のいずれかがより詳細な説明を必要とする場合-プロセスに突破口があったので、私はそれについて書きたいと思います。 「しかし、この記事のフィールドは狭すぎます」(c)