Skip to content

Component Testing ​

Component testing is a form of unit testing that targets Vue or React components. It involves testing individual components in isolation from others, but considering more of its internal workings than just single functions.

Testing a Simple Button Component in Next.JS ​

First, let’s define a simple button component that accepts a label prop and an onClick handler.

tsx
//Button.tsx
import React from "react";

interface ButtonProps {
  label: string;
  onClick: () => void;
}

const Button: React.FC<ButtonProps> = ({ label, onClick }) => {
  return <button onClick={onClick}>{label}</button>;
};

export default Button;

To test this component, we'll need to ensure that:

  • The component renders with the correct label.
  • Clicking the button triggers the correct callback function.

Ensure you have @testing-library/react installed. If not, you can install them using npm Yarn or pnpm:

bash
npm install --save-dev vitest @testing-library/react
tsx
//button.spec.tsx
import { describe, it, expect } from "vitest";
import { render, fireEvent } from "@testing-library/react";
import Button from "./Button";

describe("Button Component", () => {
  it("renders with the correct label", () => {
    const { getByText } = render(
      <Button label="Click Me" onClick={() => {}} />
    );
    const button = getByText("Click Me");
    expect(button).toBeDefined();
  });

  it("calls the onClick callback when clicked", () => {
    const onClickMock = vi.fn();
    const { getByText } = render(
      <Button label="Click Me" onClick={onClickMock} />
    );
    const button = getByText("Click Me");
    fireEvent.click(button);
    expect(onClickMock).toHaveBeenCalled();
  });
});

Test Assessment ​

  • render function: Renders the component into a virtual DOM. This function is from @testing-library/react, which provides utilities to work with DOM elements as the user would.
  • getByText: A query function from @testing-library/react that returns the first element containing the given text. Useful for asserting that elements with specific text are present in the render output.
  • fireEvent: Simulates user actions. Here, it’s used to simulate a click event on the button.
  • vi.fn(): A utility from Vitest to create a mock function. It's useful for checking whether functions have been called or what they have been called with.

Testing a Simple Pagination Component in Nuxt.JS ​

Define a simple pagination component as follows:

vue
<template>
  <div class="row justify-content-between mt-3">
    <div id="user-list-page-info" class="col-md-6">
      <span
        >Showing {{ lowerBound }} to {{ upperBound }} of
        {{ totalCount }} entries</span
      >
    </div>
    <div class="col-md-6">
      <nav aria-label="Page navigation example">
        <ul class="pagination justify-content-end mb-0">
          <li
            class="page-item"
            :class="{ active: pageNo === page }"
            v-for="page in pageList"
            :key="page"
          >
            <a class="page-link" href="#" @click.prevent="onPageChanged(page)">
              {{ page }}
            </a>
          </li>
        </ul>
      </nav>
    </div>
  </div>
</template>

<script lang="ts" setup>
const props = defineProps({
  pageNo: {
    type: Number,
    required: true,
    default: 1,
  },
  totalPages: {
    type: Number,
    required: true,
  },
  totalCount: {
    type: Number,
    required: true,
  },
  lowerBound: {
    type: Number,
    required: true,
  },
  upperBound: {
    type: Number,
    required: true,
  },
  disabled: {
    type: Boolean,
  },
});

const emit = defineEmits(["onPageChanged"]);

const { pageNo, totalPages, totalCount } = toRefs(props);

const selectedPage = ref(pageNo.value ?? 1);

const pageList = computed(() => {
  let counter = 1;
  let initPage = 1;
  const currentPage = pageNo.value;
  const pages = totalPages.value;

  if (currentPage > 9 && pages > 10) {
    initPage = currentPage - 5;
  }

  const pageNumbers = [];

  for (let i = initPage; i <= pages; i++) {
    pageNumbers.push(i);
    counter++;
    if (counter > 10) {
      break;
    }
  }

  return pageNumbers;
});

const onPageChanged = (currrentPage: number) => {
  selectedPage.value = currrentPage;
  emit("onPageChanged", selectedPage.value);
};
</script>

Test the component above using the sample code below:

typescript
import { mount } from "@vue/test-utils";
import PaginationComponent from "@/components/base/pagination.vue";
import { describe, expect, it } from "vitest";

describe("PaginationComponent", () => {
  it("renders correctly with default props", () => {
    const wrapper = mount(PaginationComponent, {
      props: {
        pageNo: 1,
        totalPages: 15,
        totalCount: 150,
        lowerBound: 1,
        upperBound: 10,
      },
    });
    expect(wrapper.text()).toContain("Showing 1 to 10 of 150 entries");
  });

  it("pageList should generate correct pages when middle pages are selected", () => {
    const wrapper = mount(PaginationComponent, {
      props: {
        pageNo: 11,
        totalPages: 15,
        totalCount: 150,
        lowerBound: 80,
        upperBound: 90,
      },
    });
    expect(wrapper.vm.pageList).toEqual([6, 7, 8, 9, 10, 11, 12, 13, 14, 15]);
  });

  it("emits onPageChanged when a page number is clicked", async () => {
    const wrapper = mount(PaginationComponent, {
      props: {
        pageNo: 1,
        totalPages: 5,
        totalCount: 45,
        lowerBound: 1,
        upperBound: 9,
      },
    });
    // Clicking the second page link assuming the first page link is the current page (page 2)
    await wrapper.findAll("a.page-link")[2].trigger("click.prevent");

    // Checking if event is emitted
    expect(wrapper.emitted()).toHaveProperty("onPageChanged");

    // Checking the payload of the emitted event
    expect(wrapper.emitted("onPageChanged")[0]).toEqual([3]); // expecting that the second page link clicked corresponds to page 3
  });
});

Another alternative is to use beforeEach to mount the Pagination Component before each test, so you don't have to repeat the propData initialization code in every test. However this will only fit into scenerios where the same parameters are relevant for all test cases. See sample code as follows:

typescript
import { mount } from "@vue/test-utils";
import Pagination from "@/components/Pagination.vue";
import { describe, beforeEach, expect, it } from "vitest";

describe("Pagination.vue", () => {
  let wrapper;

  beforeEach(() => {
    wrapper = mount(Pagination, {
      propsData: {
        pageNo: 1,
        totalPages: 15,
        totalCount: 150,
        lowerBound: 1,
        upperBound: 10,
      },
    });
  });
});