Skip to content

Testing

Before trying to run tests, make sure that you have setup the Linter for local use as described in the Setup Guide.

Testing the Linter is broken into unit tests that can be run against logic inside of the repository and integration testing which general applies to interacting with Obsidian, verifying how UI elements look, and making sure the plugin still loads.

Unit Testing

Unit tests are a great way to make sure that the rules and other logic within the Linter is working as intended and expected especially over time with code refactoring, logical changes, and bug fixes. This helps make sure that the logic still works like it did before changes were made.

Unit test reliability

Unit tests are only as reliable as the quality of the tests. So if the tests are poor and barely test the functionality of the rules or logic in question, the unit tests can give a false impression that the code is working.

What Do Unit Tests Look Like in the Linter?

There are really 2 broad categories of unit tests in the Linter: rule examples and test suites.

Rule Examples

These are the examples that you find in the definition of rules themselves. You can find more on them in Rule Examples under Adding a Rule.

Test Suites

These are tests that reside in the __tests__ directory. They can be broken into 2 categories themselves: general rule test suites and specific test suites.

General Rule Test Suites

As the name suggests, these are test suites that follow a general format and each one is specific to a rule. For example, capitalize-headings.test.ts is a general rule test suite since it only has tests for Capitalize Headings.

These rules follow the same kind of format:

import CapitalizeHeadings from '../src/rules/capitalize-headings';
import dedent from 'ts-dedent';
import {ruleTest} from './common';

ruleTest({
  RuleBuilderClass: CapitalizeHeadings,
  testCases: [
    {
      testName: 'Ignores not words',
      before: dedent`
        # h1
        ## a c++ lan
        ## this is a sentence.
        ## I can't do this
        ## comma, comma, comma
        ## 1.1 the Header
        ## état
        ## this état
      `,
      after: dedent`
        # H1
        ## A c++ Lan
        ## This is a Sentence.
        ## I Can't Do This
        ## Comma, Comma, Comma
        ## 1.1 The Header
        ## État
        ## This État
      `,
      options: {
        style: 'Title Case',
      },
    },
    ...
    { // accounts for https://github.com/platers/obsidian-linter/issues/601
      testName: `Make sure that if the 1st word has a number in it, it will still be considered to be a word and have its first letter capitalized`,
      before: dedent`
        # EC2 instance
        ## EC2 lab05 load balancer
        ### lab07 bread maker
      `,
      after: dedent`
        # EC2 instance
        ## EC2 lab05 load balancer
        ### Lab07 bread maker
      `,
      options: {
        style: 'First letter',
        ignoreCasedWords: true,
      },
    },
  ],
});

The file starts off with the imports which includes the Rule Options, any type imports it may need, and the ruleTest method which basically sets up the tests to be run as an array of tests:

import CapitalizeHeadings from '../src/rules/capitalize-headings';
import dedent from 'ts-dedent';
import {ruleTest} from './common';

After that comes the list of tests being passed into ruleTest:

ruleTest({
  RuleBuilderClass: CapitalizeHeadings,
  testCases: [
    {
      testName: 'Ignores not words',
      before: dedent`
        # h1
        ## a c++ lan
        ## this is a sentence.
        ## I can't do this
        ## comma, comma, comma
        ## 1.1 the Header
        ## état
        ## this état
      `,
      after: dedent`
        # H1
        ## A c++ Lan
        ## This is a Sentence.
        ## I Can't Do This
        ## Comma, Comma, Comma
        ## 1.1 The Header
        ## État
        ## This État
      `,
      options: {
        style: 'Title Case',
      },
    },
    ...
    { // accounts for https://github.com/platers/obsidian-linter/issues/601
      testName: `Make sure that if the 1st word has a number in it, it will still be considered to be a word and have its first letter capitalized`,
      before: dedent`
        # EC2 instance
        ## EC2 lab05 load balancer
        ### lab07 bread maker
      `,
      after: dedent`
        # EC2 instance
        ## EC2 lab05 load balancer
        ### Lab07 bread maker
      `,
      options: {
        style: 'First letter',
        ignoreCasedWords: true,
      },
    },
  ],
});

rulesTest expects the RuleBuilderClass to be the rule options class reference and then testCases which is a list of test cases for the rule. The test cases are almost identical to Rule Examples however they use testName instead of description.

Specific Test Suites

These test suites are generally tailored to a specific function that exists that is not a rule. They are generally meant to make sure that certain functions still work as intended. An example of a specific test suite is get-all-custom-ignore-sections-in-text.test.ts. The logic for these tests tries to follow a similar setup to that of a general rule test suite, but is tailored to the needs of the specific function that is being tested:

import {getAllCustomIgnoreSectionsInText} from '../src/utils/mdast';
import dedent from 'ts-dedent';

type customIgnoresInTextTestCase = {
  name: string,
  text: string,
  expectedCustomIgnoresInText: number,
  expectedPositions: {startIndex:number, endIndex: number}[]
};

const getCustomIgnoreSectionsInTextTestCases: customIgnoresInTextTestCase[] = [
  {
    name: 'when no custom ignore start indicator is present, no positions are returned',
    text: dedent`
      Here is some text
      Here is some more text
    `,
    expectedCustomIgnoresInText: 0,
    expectedPositions: [],
  },
  {
    name: 'when no custom ignore start indicator is present, no positions are returned even if custom ignore end indicator is present',
    text: dedent`
      Here is some text
      <!-- linter-enable -->
      Here is some more text
    `,
    expectedCustomIgnoresInText: 0,
    expectedPositions: [],
  },
  {
    name: 'a simple example of a start and end custom ignore indicator results in the proper start and end positions for the ignore section',
    text: dedent`
      Here is some text
      <!-- linter-disable -->
      This content will be ignored
      So any format put here gets to stay as is
      <!-- linter-enable -->
      More text here...
    `,
    expectedCustomIgnoresInText: 1,
    expectedPositions: [{startIndex: 18, endIndex: 135}],
  },
  {
    name: 'when a custom ignore start indicator is not followed by a custom ignore end indicator in the text, the end is considered to be the end of the text',
    text: dedent`
      Here is some text
      <!-- linter-disable -->
      This content will be ignored
      So any format put here gets to stay as is
      More text here...
    `,
    expectedCustomIgnoresInText: 1,
    expectedPositions: [{startIndex: 18, endIndex: 129}],
  },
  {
    name: 'when a custom ignore start indicator shows up midline, it ignores the part in question',
    text: dedent`
      Here is some text<!-- linter-disable -->here is some ignored text<!-- linter-enable -->
      This content will be ignored
      So any format put here gets to stay as is
      More text here...
    `,
    expectedCustomIgnoresInText: 1,
    expectedPositions: [{startIndex: 17, endIndex: 87}],
  },
  {
    name: 'when a custom ignore start indicator does not follow the exact syntax, it is counted as existing when it is a single-line comment',
    text: dedent`
      Here is some text<!-- linter-disable-->here is some ignored text<!-------------         linter-enable ------>
      This content will be ignored
      So any format put here gets to stay as is
      More text here...
    `,
    expectedCustomIgnoresInText: 1,
    expectedPositions: [{startIndex: 17, endIndex: 109}],
  },
  {
    name: 'multiple matches can be returned',
    text: dedent`
      Here is some text<!-- linter-disable -->here is some ignored text<!-- linter-enable -->
      This content will be ignored
      So any format put here gets to stay as is
      More text here...
      ${''}
      <!-- linter-disable -->
      We want to ignore the following as we want to preserve its format
        -> level 1
          -> level 1.3
        -> level 2
      Finish
    `,
    expectedCustomIgnoresInText: 2,
    expectedPositions: [{startIndex: 17, endIndex: 87}, {startIndex: 178, endIndex: 316}],
  },
];

describe('Get All Custom Ignore Sections in Text', () => {
  for (const testCase of getCustomIgnoreSectionsInTextTestCases) {
    it(testCase.name, () => {
      const customIgnorePositions = getAllCustomIgnoreSectionsInText(testCase.text);

      expect(customIgnorePositions.length).toEqual(testCase.expectedCustomIgnoresInText);
      expect(customIgnorePositions).toEqual(testCase.expectedPositions);
    });
  }
});

These tests general start by importing the function to test followed by the format of how the test cases will be formatted:

import {getAllCustomIgnoreSectionsInText} from '../src/utils/mdast';
import dedent from 'ts-dedent';

type customIgnoresInTextTestCase = {
  name: string,
  text: string,
  expectedCustomIgnoresInText: number,
  expectedPositions: {startIndex:number, endIndex: number}[]
};

After that comes the test cases and then the running of the test cases:

describe('Get All Custom Ignore Sections in Text', () => {
  for (const testCase of getCustomIgnoreSectionsInTextTestCases) {
    it(testCase.name, () => {
      const customIgnorePositions = getAllCustomIgnoreSectionsInText(testCase.text);

      expect(customIgnorePositions.length).toEqual(testCase.expectedCustomIgnoresInText);
      expect(customIgnorePositions).toEqual(testCase.expectedPositions);
    });
  }
});

This is the format you will want to use for specific test suites if at all possible. There are some scenarios where the test cases do vary so much that creating a type for the test case is not feasible and so individual tests are used like what rules-runner.test.ts does.

Should You Add a Test?

You may be wondering whether you should or should not add a test to the Linter. If you do any of the following, you should add a unit test:

Situation Tests to Include
Add a new rule Examples on the rule that cover general use cases

Unit tests in a test suite for the new rule that cover cases that make the examples too long or that are edge cases
Add a new option to a rule An example or examples on the rule to cover the general scenarios of the new option

Unit tests in a test suite for the rule that cover cases that make the examples too long or that are edge cases
Refactoring code This may require a new test suite that is designed specifically for the refactored code (for example get-all-tables-in-text.test.ts) or new unit tests in a test suite that uses the logic that was refactored if it has changed the edge cases that are possible
Fixing a bug When a bug directly affects a single rule and can be reproduced by setting up a case in the rule's test suite, add that rule with a comment referencing back to the issue that reported the problem*

*Here is what an example of bug fix unit test looks like in a general rule test suite:

{ // accounts for https://github.com/platers/obsidian-linter/issues/412
  testName: 'H1s become H2s and all other headers are shifted accordingly when an H1 starts a file',
  before: dedent`
    # H1
    ### H3
    #### H4
    # H1
    #### H4
    ###### H6
  `,
  after: dedent`
    ## H1
    ### H3
    #### H4
    ## H1
    ### H4
    #### H6
  `,
  options: {
    startAtH2: true,
  },
},

Adding Tests

Where to add a test depends on what kind of test you are adding. If it is an example, it should reside in the specific rule that it pertains to. If it is meant to be a bug fix, edge case test, a test for a non-rule function, or a large test, then adding it to an existing or new test suite makes the most sense.

When adding an example test case, please follow the format described by Rule Examples. When adding an test suite test case, please follow the format described above by Test Suites making sure that any newly added test suites have dashes between words in the filename.

Once a test is added, you will want to run the tests, see Running Tests.

Running Tests

Tests are run by jest and running them varies depending on whether you want to run all tests or one or more test suites.

All Tests

They can be run by either running npm run test or npm run compile. The output will let you know how many of the tests passed and if any failed, why they failed using a visual comparison of what was expected versus what was received.

Advanced tests

When advanced tests fail, their output is harder to read since it uses a regex match. It is recommended that you use the output of the expected versus actual values for the normal tests to determine what went wrong with the test.

A Specific Test Suite

If you know the suite of tests that you would like to run, you can use npm run test-suite TEST_SUITE_HERE to run just the desired test suite. The test suite names are the names of the files in __tests minus .test.ts. You only need to use part of a file name for a test suite to be used as it checks that the test suite name starts with the value of TEST_SUITE_HERE.

So for example, npm run test-suite format-yaml-arrays would run the test suite for formatting YAML arrays since that is the only test suite that starts with format-yaml-arrays. While, npm run test-suite header would run all test suites that start with the word header.

Note

Running a test suite for a specific rule or rules does not run the examples for that rule(s) as all examples are bundled together in the examples test suite which can be run via npm run test-suite examples.

Integration Testing

Integration tests are reserved for things that are not easily tested with unit tests. When doing these tests, you will need to load your local copy of the Linter into Obsidian and then run the Linter with the desired rules turned on.

When Should I Do Integration Testing?

When a rule is changed to run as part of the rules to run after or rules to run before the normal rules in rules-runner.ts.

When a UI change is made. For example a wording change or a display element changes like CSS or HTML changes.

When the issue was caused by multiple rules making changes to the contents of a file to create an issue. When this happens, the only way I have found to be reliable when testing that the issue is resolved is via integration testing.