React

Last Updated: 3/10/2024

Unit Testing

Automated Testing

Manual Testing

  • In manual testing, you have to launch your application in the browser, perhaps you have to login, or maybe you have to do a few clicks here and there to get to a page to where this function is used. Then, you will have to fill out a form, submit it, and see the result of this function on the screen.
  • You have to repeat all these steps each time using different values in your form. As you can see, this is very time consuming. - This work flow to test this function may take several minutes every time. Now to make matters worse, this is not the only function in your application. In a real application you have tens or hundreds of functions like this.
  • As your application grows, in size and complexity, the time required to manually test all the different bits and pieces increases exponentially.

What is Automated Testing

  • Automated testing is the practice of writing code to test our code, and then run those tests in an automated fashion.
  • So, with automated testing, our source code consists of application code, which we also call production code and test code.
  • With automated testing, you write code and directly call the function with different inputs and verify that the function returns the right output.
  • You can re-run these tests every time you change your code, every time you commit your code to a repository and before deploying your application. With this approach, you can test all the execution paths in this function in less than a second!
  • You can write several hundred or thousands of automated tests for various parts of your application, and run them all in just a few seconds

Benefits of Automated Testing

  • Test application code on a frequent basis and in less time.
  • Catch the bugs before deploying our application.
  • Deploy your application with more confidence.
  • Reduce the number of defects or bugs that will go in the production and improve the quality of your software.
  • Allows you to refactor your code with confidence. Refactoring means changing the structure of your code, without changing its behavior. Every time you refactor your code, you run your tests and make sure you didn't break anything that used to previously work.
  • Writing tests is that it helps you focus more on the quality of the methods that you're writing. You make sure that every method works with different inputs under varying circumstances.

Types of Testing

  • Unit Test:
    • You test a unit of the application without its external dependencies such as files, databases, message queues, web services and so on.
    • They are cheap to write and they execute fast
    • You can run hundreds of them in just a few seconds, and this way you can verify that each building block in our application is working as expected.
    • Since you're not testing these classes or components with their external dependencies, you can't get a lot of confidence in the reliability of your application
  • Integration Test:
    • Tests a class or a component with its external dependencies.
    • It tests the integration of your application code with these concrete dependencies like files, databases and so on
    • These tests, take longer to execute because they often involve reading or writing to a database, but they give us more confidence in the health of our application.
  • End-to-End Test:
    • There are specific tools built for creating end-to-end tests. eg Selenium
    • Allows us to record the interaction of a user with our application and then play it back and check if the application is returning the right result or not.
    • These tests give you the greatest amount of confidence about the health of your application
    • They are very slow.
    • They're very brittle, because a small enhancement to the application or a small change in the user-interface can easily break these tests.

Test Pyramid

  • Most of your tests should be in the category of unit tests, because these tests are easy to write, and they execute quickly.
  • Since they don't give you much confidence about the health of your application, you should have a bunch of integration tests that test the integration of your application code with its external dependencies. These tests provide many advantages of end-to-end tests, but without the complexities of dealing with the user interface.
  • You should write very few end-to-end tests for the key functions of the application, but you should not test the edge cases with these end-to-end tests. You only test the happy path, and leave the edge cases to unit tests.
  • The actual ratio between your unit integration and end-to-end tests, really depends on your project.
  • Unit tests are great for quickly testing the logic of conditional statements and loops. If you have methods with complex logic and calculation, you should test them with your unit tests.
  • Application that simply reads some data from or writes it to a database need more integration tests than unit tests.

Tooling

  • To write test you need a testing framework.
  • Framework provides library for writing unit tests and test runner for executing unit tests
  • Popular Frameworks
    • Jasmine: Early
    • Mocha: Have to use with Chai & Sinon
    • Jest: Wrapper around jasmine. Used by facebook for testing react components

Unit Test

  • Initialize project npm init -y
  • Install jest as development dependency npm install jest --save-dev
  • In package.json, scripts, test: jest
  • Run command. Jest finds files that ends with test.js or spec.js npm test

Create Test

math.test.js

test("first test", () => {});

Testing numbers

  • no of unit tests >= no of execution paths
  • use simple numbers

lib.js

module.exports.absolute = function(num) {
	if(num > 0) return num;
	if(num < 0) return -num;
	return 0;
}

lib.test.js

cons lib = require("lib);
test('absolute - should return a pos num if input is pos', () => {
 const result = lib.absolute(1);
  expect(result).toBe(1)
});

test('absolute - should return a pos num if input is neg', () => {
 const result = lib.absolute(-1);
  expect(result).toBe(1)
});

test('absolute - should return a 0 if input is 0', () => {
 const result = lib.absolute(0);
  expect(result).toBe(0)
});

Testing numbers

  • no of unit tests >= no of execution paths
  • use simple numbers

App Code

module.exports.absolute = function(num) {
	if(num > 0) return num;
	if(num < 0) return -num;
	return 0;
}

Test Code

cons lib = require("./lib);
test('absolute - should return a pos num if input is pos', () => {
 const result = lib.absolute(1);
  expect(result).toBe(1)
});

test('absolute - should return a pos num if input is neg', () => {
 const result = lib.absolute(-1);
  expect(result).toBe(1)
});

test('absolute - should return a 0 if input is 0', () => {
 const result = lib.absolute(0);
  expect(result).toBe(0)
});
  • visit jest website for more matchers

Grouping Tests

  • As we write more tests, it is important to organize tests, so they're clean and maintainable.
  • Tests are the first-class citizens in your source code.
  • In jest and jasmine, we have a function called describe for grouping a bunch of related tests.
  • We have another function in jasmine and jest, that is it
describe("absolute", () => {
	it('should return a pos num if input is pos', () => {
	 const result = lib.absolute(1);
	  expect(result).toBe(1)
	});
});

Refactoring with Confidence

  • Unit test helps you to refactor your code with confidence Refactor-1
module.exports.absolute = function(num) {
	if(num >= 0) return num;
	if(num < 0) return -num;
}

Refactor-2

module.exports.absolute = function(num) {
	return (num >= 0) ? num: 0;
}

Testing Strings

  • Your tests should neither be too specific or too general. They should be at the right balance
  • If they were too specific, they can easily break. If they are too general, they may not give you the confidence that the code is actually working.
  • For testing strings, instead of testing for the exact equality, you can look for certain patterns, you can use a regular expression.

App Code

module.exports.greet = function(name) {
	return "Welcome " + name;
}

Test Code

expect(result).toMatch(/Ganesh/);
expect(result).toContain("Ganesh");

Testing Arrays

  • Tests should not be too general nor too specific App Code
module.exports.getCurrencies = function() {
	return ["USD", "AUD", "EUR"];
}

Test Code

const result = lib.getCurrencies();

//too general
expect(result).toBeDefined();
expect(result).not.toBeNull();

//too specific
expect(result[0]).toBe("USD");
expect(result[1]).toBe("AUD");
expect(result[2]).toBe("EUR");
expect(result.length).toBe(3);

//proper way
expect(result).toContain("USD");
expect(result).toContain("AUD");

//Ideal way
expect(result).toEqual(expect.arrayContaining(['EUR', 'AUD', 'USD']);

Testing Objects

  • toBe: matcher function compares the references of the objects in memory
  • toEqual: used for object equality and source and target objects should have exactly same number of properties.
  • toMatchObject: you can add only the properties you are interested in.
  • toHaveProperty: we pass key value pairs, the type of this property is also very important Code
//object
module.exports.getProduct = function(productId) {
	return {id: productId, price: 10};
}

Test

const result = lib.getProduct(1);
//too specific
expect(result).toEqual({id: 1, price: 10});

//ideal way
expect(result).toMatchObject({id: 1, price: 10});
expect(result).toHaveProperty("id", 1);

Testing Exceptions

Code

module.exports.registerUser = function(username) {
	if(!username) throw new Error("username is required");
	return {id: new Date().getTime(), username: username};
}

Test

expect(() => {lib.registerUser(null)}).toThrow();
it("should throw if username is falsy", () => {
		const args = [null, undefined, NaN, '', 0, false];
		args.forEach(a => {
			expect(() => {lib.registerUser(a)}).toThrow();
		})
})

Watching Files

package.json

"test": "jest --watchAll"

Creating Simple Mock Function

  • The whole point of unit tests is to decouple your code from external dependencies that might not be available at the time of running your tests to you can quickly and reliably execute your tests.
  • Replace the real implementation of a method, with a fake or mock implementation
  • use simple values for testing as opposed to magic numbers or magic emails

Code

module.exports.applyDiscount = function(order) {
	const customer = db.getCustomerSync(order.customerId);
	if(customer.points > 10)
	order.totalPrice *= 0.9;
}

Test

describe("applyDiscount", () => {
	it("should apply 10% discount if customer has more than 10 points", () => {
		db.getCustomerSync = function() {
			return {id: customerId, points: 20};
		}	

		const order = {customerId: 1, totalPrice: 10};
		lib.applyDiscount(order);
		expect(order.totalPrice).toBe(9);
	});
});

Jest Mock Functions

  • In jest, we have a method for creating mock functions. jest.fn which is short for function.
  • This function has no implementation.
  • we can program this mock to return a specific value, a promise
  • we can see we just create a mock function and program it to behave a certain way. In each test, you can have a mock function that behaves differently. Code
module.exports.notifyCustomer = function(order) {
	const customer = db.getCustomerSync(order.customerId);
	mail.send(customer.email, "Your order was placed successfully");
}
  • Create mock function
const mockFunction = jest.fn();
  • For Synchronous function
mockFunction.mockReturnValue(1);
mockFunction();
  • For Asynchronous function
mockFunction.mockResolvedValue(1);
mockFunction.mockRejectedValue(new Error(""));
await mockFunction();
  • Set mock function
db.getCustomerSync = jest.fn().mockReturnValue({email: "a"});
  • Assert mock function
mail.send = jest.fn();
expect(mail.send).toHaveBeenCalled();
expect(mail.send).toHaveBeenCalledWith('a');
expect(mail.send.mock.calls[0][0]).toBe("a");
expect(mail.send.mock.calls[0][1]).toContain("order");

What to Unit Test

  • Use unit tests for testing functions with algorithms that have 0 or minimal dependency to external resources.
  • Try to avoid creating too many mocks. If you are doing too much mocking, it's better to write an integration test.

Commands

Testing a Single File

npm test -- math.test.js

Testing a single spec

npm test -- -t="sum"

Testing a single spec using only it.only("", () => {})

Jest Methods

beforeAll afterAll beforeEach afterEach

Test Structure

Test Driven Development (TDD)

  • Arrange, Act, Assert

Behavior Driven Development (BDD)

  • Given, When , Then

https://martinfowler.com/bliki/GivenWhenThen.html