Preamble
I work for a company that makes quite a lot of and, I’m not afraid of this word, a cumbersome mobile application with a solid history for several years and, therefore, with a fairly solid and monstrous code.
The flow of wishes from the customer is varied and abundant, and in connection with this, from time to time it is necessary to make changes even in those places that are not intended for this purpose, such as. Some of the problems that arise with this — regression bugs — deliver quite a few complicated clocks from time to time.
At the same time, for one reason or another, there is only manual testing and a fairly impressive number of testers on the project, and quite naive attempts to automate it remained only at the level of several rather trivial unit tests at the “Hello world” level.
In particular, the testing department has an impressive cycle of tests for regression search, which is conducted quite regularly and takes a decent amount of time. Accordingly, once the task arose to somehow optimize this process. This will be discussed.
Honestly, I do not remember which tools for automated acceptance testing I watched and why they did not fit me. (I would be very grateful if someone in the comments prompts interesting solutions to this - I probably missed something very worthwhile) I can say one thing for sure - because our application, in fact, a thin client, is very many cases impossible (or at least I don't know how to) cover with unit tests and need something else. One way or another, it was decided to write my own library to automate acceptance testing.
About the system itself and its use
So, this system must meet the following requirements:
- The system should be able to run tests in the application runtime.
- The system should enable testers to translate tests from a human language into something that can be performed automatically.
- The system must cover acceptance tests in the sense that it must provide access in one way or another to the data that the user sees.
- The system must be portable. (It is desirable that it was possible to adapt it to other platforms, except iOS and, perhaps, to other projects)
- The system should enable synchronization of tests with an external source.
- The system should provide the ability to send test results to an external service.
')
Large-block, the testing system should consist of the following blocks:

A potential tester should act as follows:
- Log in to webGUI, log in and write / edit some test cases and test plans
- Launch the application on the device and open the test interface (for example, tap with three fingers at any moment of the application)
- Get test plans and test cases from the server of interest
- Run tests. Some of them will complete correctly, some will fall, some (for example, UI tests) will require additional validation
- On webGUI, open this execution history and find tests that require additional validation and, based on additional data (for example, screenshots in those moments), write down yourself - whether the test was successful or not
In this article I would like to dwell in more detail on the TestCore module. It can be described in approximately the following scheme:

Sequencing:
- Native macros are read and placed in a macro table with keys - names with which they can be called from the test case.
- Test case files are read - they are run through the lexer and parser, on the basis of which the syntactic execution tree is built.
- Test cases are placed in a table of test cases with keys - names.
- From outside, the team comes to the execution of a certain test plan and the user sees the test result.
And from this moment more:
The test case is a structure of the following form:
.
The test action can be a macro call, arithmetic operations or assignments. For example, here are some simple examples that I used to check the correctness of the system:
The send, log, waitFor, and other operators — these are macros — about which I wrote above — are essentially native methods, the writing of which lies on the programmer’s shoulders, and not on the tester’s.
Here, for example, the logging macro code:
@implementation LogMacros -(id)executeWithParams:(NSArray *)params success:(BOOL *)success { NSLog(@"TESTLOG: %@",[params firstObject]); return nil; } +(NSString *)nameString { return @"log"; } @end
But the macro code FailOn is essentially an assertion.
@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
Thus, by writing a number of custom macros (macros from the above example and several others), it is possible to provide access to application data to check them with these tests, perform some UI actions, and send screenshots to the server.
One of the key macros is the waitFor macro, which expects the application to react to its actions. He, at the moment is one of the main points where the application code affects the execution of test examples. That is, for comfortable work of the library, it is necessary not only to write a certain number of project-specific macros, but also to introduce various status signals into the application code, about changing the state, sending a request, receiving a response, and so on. That is, in other words, prepare an application to ensure that it will be tested in this way.
Under the hood
And under the hood, the fun begins. The main part (lexer, parser, executor, execution tree) is written in C, YACC and Lex - so it can be compiled, run and quite successfully interpret the tests not only on iOS, but also on other systems that can C. If interest - I would try to write in a separate article about the intricacies of splicing non-native iOS languages with my favorite IDE Xcode - all development was done in it, but in this article I’ll only briefly tell you about the code.
Lex
As you already understood, a small, but very proud interpreted scripting language was written to solve the problem, which means that the task of interpretation arises to its full height. For a qualitative interpretation of samopisnyh bicycles means not so much and I used a bunch of YACC and LEX.
On Habré there were several articles on their topic (to be honest, they didn’t have enough to start. There was a lack of some not too complicated, but not too obvious, use example. And I would like to believe that if someone there will be such a task - my code will help to take some kind of start):
A series of articles on writing a compiler with immersion in how it works ;
A small article about one simple example ;
Wiki about lexers ;
Wiki about YACC .
Well, and many other useful and not very links ...
In short, the Lexer task is to ensure that the input to the parser is not a symbol-for-symbol, but a sequence of tokens already defined with already defined types.
In order not to litter the article with long listings - here is the code of one of the lexers:
LexerIn fact, he distinguishes between arithmetic signs, numbers and names, and he also passes them to the input of the parser.
YACC
In fact, YACC is a magic thing that translates the once written description of a
Backus-Naur Form into an interpreter of the language.
Here is the code of the main parser:
ParserConsider a piece of it, for understanding:
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 generates a syntax tree, that is, in fact, the entire test case collapses into one program node, which in turn consists of a case declaration, an action list, and a final terminal. The list of actions in turn can be minimized from function calls, arithmetic expressions, and so on. For example:
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);} ;
In particular, for example, a function call is its name, its parameters, its modifiers.
In general, YACC itself is just passing through the token nodes and folding them. In order to do something about it, logic is hung on each pass according to some syntactic construction, which in parallel creates a tree in memory that can be used later. For understanding - in YACC notation
$$ is the result that is associated with the given expression
$ 1, $ 2, $ 3 ... are the results associated with the corresponding phonemes of these expressions.
And I’ll call listWithParam, mathCall, and so on - to generate and connect the nodes in memory.
Nodes
Source codes, as generators can be read here:
Node generation logicHeading nodeIn fact, the node is required so that the graph, which they represent in aggregate, can be bypassed and, by the results of the detour, get some conclusion about the test performance. In fact, they must be an
abstract syntax tree.After folding the program expression in $$, we have just this very tree and we can calculate it.
Executor
Executor's code is stored here right here:
In fact - this is a recursive analysis of the tree in depth from left to right. Depending on its type, a node is interpreted either as a mathNode (arithmetic) or as a operationalNode (macro call, compiling a list of parameters).
The leaves of this graph are either constants (string, numeric), or variable names that form a lookup table at the initial parsing stage and acquire quick access indexes to memory cells in them, and at the computation stage they simply refer to these cells, or in the same way, via the bridge module, it requests the execution of a macro with the given name and a list of parameters (here you should not forget that the parameters should already be calculated by this point). And there are a lot of other routine and not so moments connected with the management of memory, data structures, etc.
Call example
Well, as a conclusion, I will give an example of how this miracle, in fact, is called from the native code:
-(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]; }
All execution of the test plan is carried out in a separate thread - not in the main thing - and when writing macros that require access to application data, it is recommended not to forget about it. And at the output of the Run method there will be an object of the TestHierarchy class, which contains a tree of objects of the TestCase class with the name and execution status, and of course a bundle of beeches in the logs.
As PS
Oddly enough, testers took this thing with joy and now this thing is slowly preparing for implementation on the project. It would be great then to write about this wonderful process.
The source code for the TestCore module for iOS can be found at the github link:
github.com/trifonov-ivan/testSuiteIn general, a significant part of the work was done rather for self-education, but at the same time it was brought to some logical conclusion - so I will be very grateful if you can tell me some weak points in the idea - there may be some means that are more effective this task. What do you think - is it worth developing the idea to a full-fledged test service?
Well, yes - if any of the parts would need more detailed explanations - I would try to write about it, because a breakthrough appeared in the process. “But the fields of this article are too narrow for them” (c)