With the introduction of the NAV test features in NAV 2009 SP1 it has become easier to develop automated test suites for NAV applications. An important part of an automated test is the setup of the test fixture. In terms of software testing, the fixture includes everything that is necessary to exercise the system under test and to expect a particular outcome. In the context of testing NAV applications, the test fixture mainly consists of all values of all fields of all records in the database. In a previous post I talked about how to use a backup-restore mechanism to recreate the fixture and also about when to recreate the fixture.
A backup-restore mechanism allows a test to use a fixture that was prebuild at some other time, that is, before the backup was created. In this post I'll discuss the possibility to create part of the fixture during the test itself. Sometimes this will be done inline, but typically the creation of new records will be delegated to creation functions that may be reused. Examples of creation functions may also be found in the Application Test Toolset.
As an example of a creation function, consider the following function that creates a customer:
CreateCustomer(VAR Customer : Record Customer) : Code
Customer."Customer Posting Group" := CustomerPostingGroup.Code;
This function shows some of the idiom that is used in creation functions. To return the record an output (VAR) parameter is used. For convenience the primary key is also returned. When only the primary key is needed, this leads to slightly simplified code. Compare for instance
SalesHeader.VALIDATE("Bill-to Customer No.",Customer."No.");
SalesHeader.VALIDATE("Bill-to Customer No.",CreateCustomer(Customer));
Obviously, this is only possible when the record being created has a primary key that consists of only a single field.
The actual creation of the record starts with initializing all the fields that are not part of the primary key (INIT). If the record type uses a number series (as does Customer), the record is now inserted to make sure any other initializations (in the insert trigger) are executed. Only then the remaining fields are set. Finally, the number of the created customer is returned.
When creating a record some fields will need to be given a value. There are two ways to obtain the actual values to be used: they can be passed in via parameters or they can be generated inside the creation function. As a rule of thumb when calling a creation function from within a test function, only the information that is necessary to understand the purpose of the test should be passed in. All the other values are "don't care" values and should be generated. The advantage of generating "don't care" values inside the creation functions over the use parameters is that it becomes immediately clear which fields are relevant in a particular test by simply reading its code.
For the generation of values different approaches may be used (depending on the type). The RANDOM, TIME, and CREATEGUID functions can all be used to generate values of different simple types (optionally combined with the FORMAT function).
In the case a field refers to another table a random record from that table may be selected. The example shows how to use the NEXT function to move a random number of records forward. Note that the COUNT function is used to prevent moving forward too much. Also note that If this pattern is used a lot, there may be a performance impact.
Although the use of random value makes it very easy to understand what is (not) important by reading the code, it could make failures more difficult to reproduce. A remedy to this problem is to record all the random values used, or to simply record the seed used to initialize the random number generator (the seed can be set using the RANDOMIZE function). In the latter case, the whole sequence of random values can be reproduced by using the same seed.
As an alternative to selecting random records, a new record may be created to set a field that refers to another table.
For some record types the primary key field is not generated by a number series. In such cases a simple pattern can be applied to create a unique key as illustrated by the creation function below:
CreateGLAccount(VAR GLAccount : Record "G/L Account") : Code;
IF NOT GLAccount.FINDLAST THEN
The keys are prefixed with TESTACC to make it easy to recognize the records created by the test when debugging or analyzing test failures. This creation function will generate accounts numbered TESTACC001, TESTACC002, and so forth. In this case the keys will wrap around after 999 accounts are created, after which this creation function will fail. If more accounts are needed extra digits may simply be added.
For some of the fields of a record you may want to control their values when using a creation function in your test. Instead of generating such values inside the creation function, input parameters may be used to pass them in.
One of the difficulties when defining creation functions is to decide on what and how many parameters to use. In general the number of parameters for any function should be limited. This also applies to creation functions. Parameters should only be defined for the information that is relevant for the purpose of a test. Of course that may be different for each test.
To avoid libraries that contain a large number of creation functions for each record, only include the most basic parameters. For master data typically no input parameters are required. For line data, consider basic parameters such as type, number, and amount.
In a particular test you typically want to control a slightly different set of parameters compared to the set of parameters accepted by a creation function in an existing library. A simple solution to this problem is to update the record inline after it has been returned by the creation function. In the following code fragment, for example, the sales line returned by the creation function is updated with a unit price.
When the required updates to a particular record are complex or are required often in a test codeunit, this pattern may lead to code duplication. To reduce code duplication, consider wrapping a simple creation function (located in a test helper codeunit) in a more complex one (located in a test codeunit). Suppose that for the purpose of a test a sales order needs to be created, and that the only relevant aspect of this sales order is that it is for an item and its total amount. Then a local creation function could be defined like
CreateSalesOrder(Amount : Integer; VAR SalesHeader : Record "Sales Header")
In this example a complex creation function wraps two simple creation functions. The CreateSalesHeader function takes the document type, a customer number (the customer is created here as well) as input parameters. The CreateSalesLine function takes the sales header, line type, number, and quantity as input. Here, a so-called, finder function is used that returns the number for an arbitrary item. Finder functions are a different type of helper functions that will be discussed in a future post. Finally, note that the CreateSalesLine function needs the document type and number from the header; instead of using separate parameters they are passed in together (with the SalesHeader record variable).
To summarize here is a list of tips to consider when defining creation functions:
- Return the created record via an output (VAR) parameter
- If the created record has a single-field primary key, return it
- Make sure the assigned primary key is unique
- If possible, have the key generated by a number series
- The safest way to initialize a record is to make sure all triggers are executed in the same order as they would have been executed when running the scenario from the user interface. In general (when DelayedInsert=No) records are created by this sequence :
- VALIDATE primary key fields
- VALIDATE other fields
- Only use input parameters to pass in information necessary to understand the purpose of a test
- If necessary add custom setup code inline
- Wrap generic creation functions to create more specific creation functions
- Instead of passing in multiple fields of a record separately, pass in the entire record instead
These and some other patterns have also been used for the implementation of the creation functions included in the Application Test Toolset.