Liquid testing YAML syntax

YAML basics

YAML Ain't Markup Language (YAML) is a serialization language that has steadily increased in popularity over the last few years. It's often used as a format for configuration files, but its object serialization abilities make it a viable replacement for languages like JSON. YAML has broad language support and maps easily into native data structures. It's also easy for humans to read, which is why it's a good choice for configuration. The YAML acronym was shorthand for Yet Another Markup Language. But the maintainers renamed it to YAML Ain't Markup Language to place more emphasis on its data-oriented features.

General

Whitespace and indentation

Whitespace is part of YAML's formatting. Unless otherwise indicated, newlines indicate the end of a field. You structure a YAML document with indentation. Indentation in YAML is set by 2 spaces per level of nesting. Standard YAML does not support tabs, copy pasting tabs may lead to YAML errors.

my_minimal_test:
  context:
    period: 2021-12-31
    
  data:
    periods:
      2021-12-31:
      
  expectation:
    results:
      some_result: 123

Comments

Comments begin with a pound sign. They can appear after a document value or take up an entire line.

# what follows is our second test scenario
my_elaborate_test:

YAML datatypes

The key-value pair is YAML's basic building block. Every item in a YAML document is a member of at least one dictionary. The key is always a string. The value is a scalar so that it can be any datatype. So, the value can be a string, a number, or another dictionary.

Numeric values

YAML recognizes numeric types such as floating point, integer, decimal, hexadecimal or octal.

people:
  ted_123: # external_id
    amount_of_shares: 150
    amount_of_votes: 50
               
expectation:
  reconciled: true
  results:
    negative_non_deductible_taxes: 0
    code_1201_D0: 10000.0

Strings

YAML strings are Unicode. In most situations, you don't have to specify them in quotes (see specifics below).

data:
  # company data
  company:
    name: Acme Corporation
    city: Gent
    street: Lippenslaan 51

String values can span more than one line. With the fold (greater than) character, you can specify a string in a block. The pipe character has a similar function, but YAML interprets the field exactly as is.

# configuration
configuration: |
  {% assign text = "something" %}
  {% assign number = 123 %}

Boolean

YAML indicates boolean values with the keywords true. False is indicated with false.

# reconciliation data
reconciliations:
  a_handle:
    starred: true

Arrays

You can specify arrays or lists on a single line between brackets and separated by a comma sign.

data:
  periods:
    2021-09-30:
      reconciliations:
        recon:
          results:
            an_array: [null, false, 123, "123"]

Null

You enter nulls with unquoted null string literal or by leaving the result empty. However, as a best practice we would recommend the use of null.

expectation:
  results:
    test_null: null

NaN or infinity

Also NaN (not a number) or infinity elements are supported in YAML.

expectation:
  reconciled: true
  results:
    # both work
    test_inf: .NaN
    test_inf_2: .inf

Anchors and aliases

If you have repeated sections in your .yml file, you might like to use YAML anchors. They can reduce effort and make updating in bulk easier.

There are 3 parts to this:

  • The anchor & which defines a chunk of code
  • The alias * used to refer to that chunk elsewhere
  • You can use overrides with the characters <<: to add more values, or override existing ones.
periods:
  2020-12-31: &period_blueprint
    # account data
    accounts:
      "4000000":
        name_en: "Customers"
        name_nl: "Klanten"
        id: 123456
        value: 500
        custom:
          some_array.some_array_1: "foo"
               
  2021-12-31:
    <<: *period_blueprint
    accounts: # override account value, but merge name, id and custom value
      "4000000":
        value: 666

Silverfin specific YAML basics

Code base

# unique tests in test suite
my_minimal_test:
  context:
    # end date of the period for which the code will be run
    period: 2021-12-31
  data:
    periods:
      2021-12-31:
  expectation:
    reconciled: false
    results:
      some_result: 123

# second unique tests in test suite
my_elaborate_test:
  context:
    # end date of the period for which the code will be run
    period: 2021-12-31
    # end date of the period to which data will be rollforwarded
    rollforward_period: 2022-12-31
    # UI language
    locale: "nl"
    # configuration
    configuration: |
      {% assign text = "something" %}
      {% assign number = 123 %}
    
  data:
    # company data
    company:
      name: "Acme Corporation"
      company_type: "consolidation"
      company_form: "N.V."
      currency: "EUR"
      country: "BE"
      city: "Gent"
      street: "Lippenslaan 51"
      vat_identifier: "BE-0844.568.333"
      file_code: "12345"
      sync_reference: "ref"
      periods_per_year: 4
      # Special book years
      year_end: 2019-12-31
        
    special_book_years:
      2019-01-01: 2019-12-31
      2020-01-01: 2020-12-31
      
    periods:
      # previous period exists
      2019-12-31:      
      
      # populate data for current period
      2020-12-31: &period_blueprint
        # account data
        accounts:
          "4000000":
            name_en: "Customers"
            name_nl: "Klanten"
            id: 123456 # only once, should be set on the first definition of an account in a company
            value: 500
            custom:
              some_array.some_array_1: "foo"
        # reconciliation data
        reconciliations:
          a_handle:
            starred: true
            # regular custom drop
            custom:
              some.thing: "foo"
              other.thing: "555"
            # namespace as account.id
            custom:
              123456.thing: "some value"
            # account collection
            custom:
              account.selector: "#123456789,#234567890"  
            # as:date input
            custom:
              some.date: "2020-12-31"
            # fori input / custom collection  
            custom:
              non_deduct_taxes.non_deduct_tax_1:
                value: "10000"
                py_value: "9000"
              non_deduct_taxes.non_deduct_tax_2:
                value: "11000"
                py_value: "8000"
              non_deduct_taxes.non_deduct_tax_3:
                value: "12000"
                py_value: "7000"
            # as:boolean input
            custom:
              some.bool: true
          # results from other templates
          a_different_handle:
            results:
              a_null: null
              a_bool: true
              a_number: 123
              a_string: "123"
              an_array: [null, false, 123, "123"]
              # custom from other templates
              custom:
                some_custom.input_from_other: "template
          # people drop
          people:
            ted_123: # external_id
              name: "Ted"
              address_1: "some address line"
              shareholder: "true"
              director: "true"
              amount_of_shares: 150
              amount_of_votes: 50
              director_start_date: "2013-01-11"
              payload: # custom methods
                description: "a description"
                payroll_id: 1111
                phone: "555-1234"
      2021-12-31:
        <<: *period_blueprint
        accounts: # override accounts, but merge reconciliations
          "4000000":
            value: 666
      2022-12-31: # added for rollforward purposes
    
  # expected results
  expectation:
    reconciled: true
    rollforward:
      # regular input
      123456.thing: "foo"
      # fori
      non_deduct_taxes.non_deduct_tax_1:
        value: "10000"
        py_value: "9000"
    results:
      test_null: null
      test_bool: true
      test_number: 123
      test_string: "123"
      test_array: [null, false, 123, "123"]
      test_date: "2021-31-12"

Whitespace

As indicated, whitespace is part of YAML's formatting and you should structure a YAML document with indentation. Furthermore, YAML does not support tabs. However, our Ace editor inserts spaces when you press the TAB key, so when writing YAML code in our Silverfin editor you can use the tab key. Just be careful when storing or copy pasting the code elsewhere.

data:
  periods:
    2021-12-31:
      # account data
      accounts:
        "4000000":
          name_en: "Customers"
          name_nl: "Klanten"
          id: 123456 # only once, must be given early
          value: 500
          custom:
            some_array.some_array_1: "foo"

Minimal requirements

Test name, context period, data period, at least one expected result and reconciliation status should be provided to run a minimal test.

Note that in some cases, rollforward elements could also be mandatory (see below).

my_minimal_test:
  context:
    period: 2021-12-31
    
  data:
    periods:
      2021-12-31:
      
  expectation:
    reconciled: true
    results:
      some_result: 123

Custom inputs

Even though YAML is Unicode and you don’t have to specify quotes to indicate string values in most cases, you will have to do this for all custom inputs (as:integer, as:currency,, as:account_collection, etc.) except for booleans. For boolean inputs the general rule applies (i.e. true or false without quotation marks).

Note that this is Silverfin specific behavior in YAML because of the way we store text property payloads.

Liquid code:

{% input custom.some.value as:currency %}
{% assign value = custom.some.value %}
{% result 'value' value %}

{% input custom.some.bool as:boolean %}
{% result 'boolean' custom.some.bool %}

Test suit:

reconciliations:
  test_robin:
    custom:
      some.value: "300"
    custom:
      some.bool: true

Use of account id and account name

As soon as an account is created, the id and/or name can’t be overwritten in following periods/rows.

If you create an account with elements id and/or name in one period/row, you should note that the elements id and name are the same for other periods/rows and these cannot be overwritten. This also means that if you create an account without id and/or name in one period/row, you cannot add an id and/or name in the next period/row.

data:
  periods:
    2017-12-31:      
      accounts:
        "400000":
          name: "Customers"
          value: 500
          id: 982597
    2018-12-31:
      accounts:
        "400000":
          name: "Customers"
          value: 600
          id: 982597
          # id and name should be repeated from previous period and cannot be overwritten!

Company, account and people data

General rule applies, you don’t have to specify quotes to indicate string values (but you can if you want). Note that integer inputs need to be added without quotation marks (such as account value, amount of shares, etc.).

# both work
company:
  name: "Acme Corporation"
        
company:
  name: Acme Corporation  

# custom inputs need quotes (except booleans)
custom:
  reserves.text: "Hello"
  reserves.value: "200"
  reserves.boolean: true
accounts:
  "4000000":
    # both work
    name_en: Customers
    name_nl: "Klanten"
    id: 123456 # only once, must be given early
    value: 500

The people drop is a bit different from the rest. For methods already defined on the drop such as name, email, address_1... it follows the same structure as other drops (indented under the person's external id).
Custom methods, however, need to go under a block called payload as seen in the example below:

people:
  # both work
  ted_123: # external_id
    name: "Ted"
    address_1: "Some address line"
    payload: # custom methods (e.g. person.custom.payroll_id)
      payroll_id: 001
        
  robin_456: # external_id
    name: "Robin the great"
    address_1: "Biggest house of the street 12"
    payload: # custom methods (e.g. person.custom.payroll_id)
      payroll_id: 002

Company type

The key company_type under the company data can contain three values: regular, analytical, consolidation, small

data:
  company:
    company_type: "regular"

This relates to the following dropdown in the company data in the UI:

479479

Results

  • String results should be defined with quotation marks
  • integer results should be defined without quotation marks
{% input custom.some.value as:currency %}
{% assign value = custom.some.value %}
{% result 'value' value %}
reconciliations:
    test_robin:
      custom:
        some.value: "300"

expectation:
  reconciled: true
  results:
    value: 300

Reconciled

Like mentioned, the reconciliation status of the template is a mandatory expectation that should be provided in the minimal test scenario. Note that there is a slight difference in the reconciliation status calculation for liquid tests purposes compared to the reconciliation status of a template in input view. For liquid testing purposes, we do not take into consideration reconciliation type (i.e. reconciliation necessary but also valid without data, no reconciliation necessary, or reconciliation necessary and invalid without data), we simply look for the sum of all the unreconciled tags.

In case there are no unreconciled tags in the code, the reconciled status will be true for liquid testing purposes.

As a best practice and to improve readability, we would recommend to always have reconciled before results in the test suite.

reconciled: true
results:
  key: :value

Rollforward

Another element that should be added to the expectation section is the rollforward in case there is rollforward logic in your template (i.e. there are rollforward parameters in debug mode).

Related to the rollforward expectation, in the context section the rollforward_period can be defined. The rollforward_period identifies to which period data is being rollforwarded. This is not a mandatory element, so if it's not explicitly defined in the context section a default rollforward_period will be set (i.e. the current period). Note that in case a rollforward_period is specifically added to the context section, that particular period should then also be included in the data section.

Note that in case of fori rollforwards, also the last loop is being rollforwarded to nill and should therefore also be specifically added to the expectation section.

Finally, as for results, string values should be defined with quotation marks, integer values should be defined without quotation marks for the rollforward expectation section.

my_test:
  context:
    period: 2020-12-31
    rollforward_period: 2021-12-31

  data:
    periods:
      2020-12-31:
        reconciliations:
          silverfin_testing:
            custom:
              lines.line_1:
                item_1: "foo"
                item_2: "300"
      2021-12-31:
            
  expectation:
    reconciled: true
    rollforward:
        lines.line_1:
            item_1: "foo"
            item_2: 300
        lines.line_2:
            item_1: 
            item_2: 
    results:
      name: true

Account collection

There are three ways to populate an input as:account_collection :

  • Account number
  • Account id (as documented in debug section, e.g.: #982597)
  • Full number (e.g.: 1000123$)

We would recommend to use either account number or account id when writing liquid tests.

data:
  periods:
    2017-12-31:
      # account data
      accounts:
        "110100.000":
          name_en: "Customers"
          value: 500
          id: 982597
reconciliations:
  bank_recon:
    # custom drop as account collection
    custom:
      account.selector: "110100.000"
reconciliations:
  bank_recon:
    # custom drop as account collection
    custom:
      account.selector: "#982597"

Database data from other template

Whenever a custom drop is accessed from another template, this must be defined in the test, e.g.:

{% assign some_value = period.reconciliations.other_template.custom.specific.resident 
| default:"Resident" %}

You should always include the handle of the template in the data section of your test to create a dependency. Even when in the liquid code a default value is assigned or when the custom drop is not relevant for your liquid tests. If you don’t do this you will get a (liquid) error. Which makes sense, as the liquid code is run as if the other template is not included in the company.

# data from other templates
  other_template:

Results from other reconciliation templates

When your liquid code fetches results from other template, these results should be defined in your liquid test if no default is added in the liquid code (because otherwise it’s just empty).

{% assign value = period.reconciliations.other_template.results.value %}
reconciliations:
  other_template:
    results:
      value: "300"

Special book years

In case you want to run tests for broken or prolonged book years you should make use of the year_end and special_book_years element as such:

data:    
  company:    
    year_end: 2019-12-31      
  special_book_years:
    2019-01-01: 2019-12-31
    2020-01-01: 2022-09-30
  periods:
    2022-09-30:

As our database does not store period.year_start_dates, we have to mimic the calculation that happens in the backend, based on the values provided here:

  • The year_end key equals the value mentioned in “Select the client’s end of first bookkeeping year” and bookyears consisting of 12 months will automatically be calculated based on this date. In the UI, these automatic bookyears are mentioned in “Fiscal years“ in light grey text.
  • If you need to define an irregular bookyear (bookyear != 12 months), you can define these using the special_book_years key. In the UI, a special_book_year is the same as a bookyear in black text in “Fiscal years”.

A periods key with a date is also always required when defining bookyears, otherwise no ledgers are created.

As a best practice when using these special bookyears, make sure year_end matches the end of the first bookyear.

10041004

Date format

In general, all places where you set a date the date format should be yyyy-mm-dd. However, for custom inputs also other date formats are allowed like: dd-mm-yyyy, dd/mm/yyyy and yyyy/mm/dd.

In order to avoid confusion and to make sure the YAML code is uniform, we would prefer to use the date format yyyy-mm-dd in all situations. When used in custom inputs or in results, always use quotation marks as date objects are always converted as a string.

my_elaborate_test:
  context:
    # period for which the code will be run
    period: 2021-12-31
  data:
    company:
      year_end: 2021-12-31
    periods: 
      2021-12-31:
        reconciliations:
          handle:
            custom:
              some.date: "2020-12-31"
  expectation:
    reconciled: true
    results:
      test_date: "2021-12-31"

Previous period

To make sure data from previous period(s) is included or to simply make sure the liquid method exists on the period drop returns true, you should just include the previous period in the data section.

data:
  company:
    year_end: 2021-12-31
  periods:
    2020-12-31:
    2021-12-31:
     reconciliations:
       handle:

Future period

To make sure rollforward logic can be tested the future period should be included in the data section.

context:
  # period for which the code will be run
  period: 2021-12-31
  rollforward_period: 2022-12-31
data:
  company:
    year_end: 2021-12-31
  periods: 
    2021-12-31:
      reconciliations:
        handle:
          custom:
            some.date: "2020-12-31"
    2022-12-31:
expectation:
  reconciled: true
  rollforward:
    some.date: "2020-12-31"
  results:
    test_date: "2021-12-31"

Extra information

More information and basis for this document can be found here