NativeScript-Vue unit testing with Vitest
Tags: nativescript, vue, vitest, javascript
Published: 2023-12-11 22:31:04

NativeScript (NS) is a framework for building native applications using JS/TS. If we combine NS with Vue we can build native high-performance applications and also, due to the incredible architecture of NS, NS-Vue and Vue, we can use Vue dependencies as if it were a frontend application. In this case we focus on unit testing with Vitest, a testing framework that is gaining more traction every day, created by the Vue team but made to be able to test almost all the frameworks that we use together with its older brother Vite.

### Setup

The first thing to do is install the necessary dependencies for the tests.

```cli
npm i -D @types/node@18.19.1 @vitejs/plugin-vue @vue/test-utils vitest jsdom
```

Now that we have the dependencies, we start with the code. Before starting with the configuration of `vitest` we will create two files that we will use to configure the tests.

To start, for this sample we will create the `test` folder in the root directory of the project in which we will add our tests and files for the general configuration. Inside this folder we create the file `NSMockViews.ts` which will contain an array of the views offered by NativeScript. If you have any views of a plugin you can add it to this list. We will use this file in the vitest configuration.

```ts
// test/NSMockViews.ts
export const NSMockViews = [
  'AbsoluteLayout',
  'ActionBar',
  'ActionItem',
  'ActivityIndicator',
  'Button',
  'ContentView',
  'DatePicker',
  'DockLayout',
  'FlexboxLayout',
  'FormattedString',
  'Frame',
  'GridLayout',
  'HtmlView',
  'Image',
  'Label',
  'ListPicker',
  'ListView',
  'NavigationButton',
  'Page',
  'Placeholder',
  'Progress',
  'ProxyViewContainer',
  'RootLayout',
  'ScrollView',
  'SearchBar',
  'SegmentedBar',
  'SegmentedBarItem',
  'Slider',
  'Span',
  'StackLayout',
  'Switch',
  'TabView',
  'TabViewItem',
  'TextField',
  'TextView',
  'TimePicker',
  'WebView',
  'WrapLayout',
  'Prop',
  'Template',
];
```

Now we will create the `setup.ts` file inside the test folder. In this file we mock `nativescript-vue` with `@vue/test-utils` and we will also mock `@nativescript/core`.

```ts
// test/setup.ts
import Vue from '@vue/test-utils';
import { vi } from 'vitest';

/* MOCK Vue & Nativescript */
vi.mock('nativescript-vue', () => Vue);
vi.mock('@nativescript/core', async () => {
  return {
    default: () => ({}),
    TouchManager: {},
  };
});
```

So that TypeScript doesn't complain in our tests, we'll add the test folder to `tsconfig.json` in the `include` section.

```diff
// tsconfig.json
"include": [
    "src",
    "types",
+   "test"
],
```

With all the base configuration for our tests, we create the `vitest.config.ts` file that vitest will use to launch our tests.

```ts
// vitest.config.ts
import Vue from '@vitejs/plugin-vue';
import path from 'path';
import { defineConfig } from 'vitest/config';
import { NSMockViews } from './test/NSMockViews';

export default defineConfig({
  plugins: [
    Vue({
      template: {
        compilerOptions: {
          isCustomElement: (tag) =>
            NSMockViews.map((nsView) => nsView.toLowerCase()).includes(
              tag.toLowerCase()
            ),
        },
      },
    }),
  ],
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
      '~': path.resolve(__dirname, './src'),
    },
    extensions: ['.mjs', '.js', '.ts', '.json', '.vue'],
  },
  test: {
    globals: true,
    name: 'jsdom',
    environment: 'jsdom',
    setupFiles: ['test/setup.ts'],
  },
});
```

We briefly explain what we have configured here:

- We use NSMockViews to tell vue that NativeScript components are customElements.
- We added the default NativeScript-Vue aliases so that it resolves the components of our application.
- We indicate the base configuration of the tests.

### Build test and run

We now have everything configured. Let's continue creating a basic component that will show an icon to do the tests later on this component.

```vue
// src/components/Icon.vue

<script lang="ts" setup>
const props = defineProps({ icon: String });
</script>

<template>
  <Label :text="props.icon" class="m-icon-round"></Label>
</template>
```

Let's take action, let's do our first test. We create the file `test/components/icon.test.ts` and add some basic tests, such as checking that the component receives a prop and the NS `Label` view has the text that we are passing to the component.

```ts
// test/components/icon.test.ts
import Icon from '@/components/Icon.vue';
import { mount, VueWrapper } from '@vue/test-utils';
import { beforeEach, describe, expect, test } from 'vitest';

let wrapper: VueWrapper<any, any>;

beforeEach(async () => {
  wrapper = mount(Icon, {
    props: {
      icon: 'my-icon',
    },
  });
});

describe('Icon Component', async () => {
  test('mount component', async () => {
    expect(wrapper.html()).toBeTruthy();
  });

  test('should have icon as text', async () => {
    expect(wrapper.attributes('text')).toContain('my-icon');
  });

  test('should update icon ', async () => {
    expect(wrapper.attributes('text')).toContain('my-icon');
    await wrapper.setProps({ icon: 'updated-icon' });
    expect(wrapper.attributes('text')).toContain('updated-icon');
  });
});
```

We already have our first test. Let's add the script to launch vitest in our `package.json` file.

```json
"scripts": {
  "test": "vitest"
},
```

We are ready to launch the tests, now you just have to launch the tests and you should have 3 tests passing.

```cli
npm run test
```

And that's it! We only had to make a couple of mocks and define the NativeScript views as customElemts.

Here I leave a repository with a base project ready to launch the tests with vitest: https://github.com/vallemar/nativescript-vue-vitest

Happy Testing!