Testing Pure Functions

What is a Pure Function?

Let’s start by testing pure functions. A pure function is a special kind of function that:

  1. Always gives you the same output when you give it the same input For example: A function that adds two numbers will always give you 4 when you input 2 and 2

  2. Only works with the data you give it It doesn’t check anything else (like what’s in localStorage or what’s on the webpage)

  3. Doesn’t change anything outside the function It just takes some input and returns something new

Here’s a simple example of a pure function:

javascript
	function addNumbers(a, b) {
	  return a + b;
	}

Pure functions are perfect for learning how to write tests because:

  • They’re predictable (same input always gives same output)
  • They’re easy to test (you just need to check if the output is correct)
  • They don’t depend on anything else in your program

For this part of the lesson, we’ll be using examples from this GitHub repository:

https://github.com/NoroffFEU/workflow-repo

The functions we’ll be testing are located in the src/js/utils/ directory of the repository.

Testing the validateEmail function

You can find this function in the src/js/utils/validation.js file:

javascript
	export function validateEmail(email) {
	  const emailRegex = /^[^\s@]+@(stud\.noroff\.no|noroff\.no)$/;
	  return emailRegex.test(email);
	}

This function checks if an email address belongs to a Noroff student or staff member. Here is what it does:

  • If the email ends with “stud.noroff.no”, it’s a valid student email
  • If it ends with “noroff.no”, it’s a valid staff email
  • Any other email address is not valid

For example:

Now, let’s write tests for this function in validation.test.js:

javascript
	import { expect, describe, it } from "vitest";
	import { validateEmail } from "./validation";
	
	describe("validateEmail", () => {
	  // Test 1: Make sure student emails work
	  it("returns true for valid student Noroff email", () => {
	    const email = "student@stud.noroff.no";
	    const result = validateEmail(email);
	    expect(result).toBe(true);
	  });
	
	  // Test 2: Make sure staff emails work
	  it("returns true for valid Noroff staff email", () => {
	    const email = "teacher@noroff.no";
	    const result = validateEmail(email);
	    expect(result).toBe(false);
	  });
	
	  // Test 3: Make sure other email domains are rejected
	  it("returns false for non-Noroff email", () => {
	    const email = "student@gmail.com";
	    const result = validateEmail(email);
	    expect(result).toBe(false);
	  });
	
	  // Test 4: Make sure invalid email formats are rejected
	  it("returns false for invalid email format", () => {
	    const email = "not-an-email";
	    const result = validateEmail(email);
	    expect(result).toBe(false);
	  });
	});

In these tests:

  1. We group related tests using describe.
  2. Each it function describes a specific behavior we’re testing.
  3. We follow the Arrange-Act-Assert pattern in each test:
    • Arrange: Set up the email to test
    • Act: Call the validateEmail function
    • Assert: Check if the result is what we expect

This approach helps us thoroughly test our validateEmail function for different scenarios.

Testing the validatePassword function

Now let’s test the validatePassword function. Here’s the function in validation.js:

javascript
	export function validatePassword(password) {
	  return password.length >= 8;
	}

And here are the tests in validation.test.js:

javascript
	import { validatePassword } from "./validation";
	
	describe("validatePassword", () => {
	  const testCases = [
	    { password: "short", expected: false },
	    { password: "exactly8", expected: true },
	    { password: "longerpassword", expected: true },
	  ];
	
	  testCases.forEach(({ password, expected }) => {
	    it(`returns ${expected} for password "${password}"`, () => {
	      const result = validatePassword(password);
	      expect(result).toBe(expected);
	    });
	  });
	});

In these tests:

  1. We define an array of test cases, each with a password and the expected result.
  2. We use forEach to run the same test for each case.
  3. The test description changes based on the password and expected result.
  4. We check if the function returns the expected result for each password.

This approach allows us to test multiple scenarios efficiently.

Testing the validateForm function

Finally, let’s test the validateForm function. Here’s the function in validation.js:

javascript
	export function validateForm(email, password) {
	  const errors = {};
	  if (!validateEmail(email)) {
	    errors.email = "Please enter a valid Noroff email address";
	  }
	  if (!validatePassword(password)) {
	    errors.password = "Password must be at least 8 characters";
	  }
	  return {
	    isValid: Object.keys(errors).length === 0,
	    errors,
	  };
	}

And here are the tests in validation.test.js:

javascript
	import { validateForm } from "./validation";
	
	describe("validateForm", () => {
	  // We're testing three different situations:
	  const testCases = [
	    {
	      // Situation 1: Everything is correct
	      email: "valid@stud.noroff.no",
	      password: "validpass",
	      expected: { isValid: true, errors: {} },
	    },
	    {
	      // Situation 2: Everything is wrong
	      email: "invalid@gmail.com",
	      password: "short",
	      expected: {
	        isValid: false,
	        errors: {
	          email: "Please enter a valid Noroff email address",
	          password: "Password must be at least 8 characters",
	        },
	      },
	    },
	    {
	      // Situation 3: Email is good but password is too short
	      email: "valid@noroff.no",
	      password: "short",
	      expected: {
	        isValid: false,
	        errors: {
	          password: "Password must be at least 8 characters",
	        },
	      },
	    },
	  ];
	
	  testCases.forEach(({ email, password, expected }) => {
	    it(`validates correctly for email "${email}" and password "${password}"`, () => {
	      const result = validateForm(email, password);
	      expect(result).toEqual(expected);
	    });
	  });
	});

In these tests:

  1. We define test cases with different combinations of email and password.
  2. Each case includes the expected result (whether the form is valid and any error messages).
  3. We use forEach to run the same test structure for each case.
  4. We use toEqual instead of toBe because we’re comparing objects.

This approach tests the validateForm function thoroughly, checking how it handles various combinations of valid and invalid inputs.

What We Learned

In this lesson, we:

  • Understood what pure functions are and why they’re good for testing
  • Learned how to test email validation
  • Practiced testing password validation
  • Created tests for form validation that returns objects with error messages

Remember:

  • Test both valid and invalid inputs
  • Test all possible scenarios (like our three form validation cases)
  • Use clear test descriptions that explain what you’re testing
  • Group related test cases together