👨🏼‍💻

khriztianmoreno's Blog

Home Tags About |

Posts with tag testing

How to mock an HTTP request with Jest đź’»

2024-05-07
javascripttestingnodejsjestweb-development

Today I wanted to show you how to properly write a test.But anyone can figure out how to run a simple test. And here, we're looking to help you find answers you won't find anywhere else.So I thought we'd take things one step further.Let's run a more complex test, where you'll have to mock 1 or 2 parts of the function you're testing.[In case you're new here: mock is like using a stunt double in a movie. It's a way to replace a complicated part of your code (like calling an API) with something simpler that pretends to be real, so you can test the rest of your code easily.]MY testing framework of choice is Jest, because it makes everything so much easier:Zero Configuration: One of the main advantages of Jest is its zero-configuration setup. It is designed to work out of the box with minimal configuration, making it very attractive for projects that want to implement tests quickly and efficiently.Snapshot Testing: Jest introduced the concept of Snapshot Testing, which is particularly useful for testing UI components. It takes a snapshot of a component’s rendered output and ensures that it doesn’t change unexpectedly in future tests.Built-In Mocking and Spies: Jest comes with built-in support for mock functions, modules, and timers, making it easy to test components or functions in isolation without worrying about their dependencies.Asynchronous Testing Support: Jest supports asynchronous testing out of the box, which is essential for testing in modern JavaScript applications that often rely on asynchronous operations like API calls or database queries.Image descriptionDe todos modos, entremos en las pruebas:Step 1: Setting up your projectCreate a new project directory and navigate to itInitialize a new npm project: npm init -yInstall Jest: npm install --save-dev jestInstall axios to make HTTP requests: npm install axiosThese are the basic requirements. Nothing new or fancy here. Let's get started.Step 2: Write a function with an API callNow, let's say you log into some kind of application. StackOverflow, for example. Most likely, at the top right you'll see information about your profile. Maybe your full name and username, for example.In order to get these, we typically have to make an API call to get them. So, let's see how we would do that.Create a file called user.jsInside user.js, write a function that makes an API call. For example, using axios to retrieve user data:// user.js import axios from "axios"; export const getUser = async (userId) => { const response = await axios.get(`https://api.example.com/users/${userId}`); return response.data; };Step 3: Create the Test FileOkay, now that we have a function that brings us the user based on the ID we requested, let's see how we can test it.Remember, we want something that works always and for all developers.Which means we don't want to depend on whether the server is running or not (since this is not what we are testing).And we don't want to depend on the users we have in the database.Because in my database, ID1 could belong to my admin user, while in your database, ID1 could belong to YOUR admin user.This means that the same function would give us different results. Which would cause the test to fail, even though the function works correctly.Read on to see how we tackled this problem using mocks.Create a file called user.test.js in the same directory.Inside this file, import the function you want to test:import axios from "axios"; jest.mock("axios"); import { getUser } from "./user";Write your test case, mock the call, and retrieve mock data.test("should fetch user data", async () => { // Mock data to be returned by the Axios request const mockUserData = { id: "1", name: "John Doe" }; axios.get.mockResolvedValue({ data: mockUserData }); // Call the function const result = await getUser("1"); // Assert that the Axios get method was called correctly expect(axios.get).toHaveBeenCalledWith("https://api.example.com/users/1"); // Assert that the function returned the correct data expect(result).toEqual(mockUserData); });Step 4: Run the testAdd a test script to your package.json:"scripts": { "test": "jest" }Run your tests with npm test.Step 5: Review the resultsJest will display the result of your test in the terminal. The test should pass, indicating that getUser is returning the mocked data as expected.Congratulations, you now have a working test with Jest and Mocking.I hope this was helpful and/or made you learn something new!Profile@khriztianmoren

Are you making THESE unit testing and mocking mistakes?

2024-04-08
javascripttestingweb-development

Testing is hard.And it doesn't matter if you're an experienced tester or a beginner...If you've put significant effort into testing an application...Chances are you've made some of these testing and mocking mistakes in the past.From test cases packed with duplicate code and huge lifecycle hooks, to conveniently incorrect mocking cases and missing and sneaky edge cases, there are plenty of common culprits.I've tracked some of the most popular cases and listed them below. Go ahead and count how many of them you've done in the past.Hopefully, it'll be a good round.Why do people make mistakes in testing in the first place?While automated testing is one of the most important parts of the development process...And unit testing saves us countless hours of manual testing and countless bugs that get caught in test suites...Many companies don't use unit testing or don't run enough tests.Did you know that the average test coverage for a project is ~40%, while the recommended one is 80%?Image descriptionThis means that a lot of people aren't used to running tests (especially complex test cases) and when you're not used to doing something, you're more prone to making a mistake.So without further ado, let's look at some of the most common testing errors I seeDuplicate CodeThe three most important rules of software development are also the three most important rules of testing.What are these rules? Reuse. Reuse. Reuse.A common problem I see is repeating the same series of commands in every test instead of moving them to a lifecycle hook like beforeEach or afterEachThis could be because the developer was prototyping or the project was small and the change insignificant. These cases are fine and acceptable.But a few test cases later, the problem of code duplication becomes more and more apparent.And while this is more of a junior developer mistake, the following one is similar but much more clever.Overloading lifecycle hooksOn the other side of the same coin, sometimes we are too eager to refactor our test cases and we put so much stuff in lifecycle hooks without thinking twice that we don't see the problem we are creating for ourselves.Sometimes lifecycle hooks grow too large.And when this happens......and you need to scroll up and down to get from the hook to the test case and back...This is a problem and is often referred to as "scroll fatigue".I remember being guilty of this in the past.A common pattern/practice to keep the file readable when we have bloated lifecycle hooks is to extract the common configuration code into small factory functions.So, let's imagine we have a few (dozens of) test cases that look like this:describe("authController", () => { describe("signup", () => { test("given user object, returns response with 201 status", async () => { // Arrange const userObject = { // several lines of user setup code }; const dbUser = { // several lines of user setup code }; mockingoose(User).toReturn(undefined, "findOne"); mockingoose(User).toReturn(dbUser, "save"); const mockRequest = { // several lines of constructing the request }; const mockResponse = { // several lines of constructing the response }; // Act await signup(mockRequest, mockResponse); // Assert expect(mockResponse.status).toHaveBeenCalled(); expect(mockResponse.status).toHaveBeenCalledWith(201); }); test("given user object with email of an existing user, returns 400 status - 1", async () => { // Arrange const userObject = { // several lines of user setup code }; const dbUser = { // several lines of user setup code }; const mockRequest = { // several lines of constructing the request }; const mockJson = jest.fn(); const mockResponse = { // several lines of constructing the response }; mockingoose(User).toReturn(dbUser, "findOne"); // Act await signup(mockRequest, mockResponse); // Assert expect(mockResponse.status).toHaveBeenCalled(); expect(mockResponse.status).toHaveBeenCalledWith(400); expect(mockJson).toHaveBeenCalled(); expect(mockJson).toHaveBeenCalledWith({ status: "fail", message: "Email taken.", }); }); }); });We can extract the repeated configuration information into its own functions called createUserObject, createDbUserObject and createMocksAnd then the tests would look like this:test("given user object, returns response with 201 status", async () => { const userObject = createUserObject(); const dbUser = createDbUserObject(); const [mockRequest, mockResponse] = createMocks(userObject); mockingoose(User).toReturn(undefined, "findOne"); mockingoose(User).toReturn(dbUser, "save"); await signup(mockRequest, mockResponse); expect(mockResponse.status).toHaveBeenCalled(); expect(mockResponse.status).toHaveBeenCalledWith(201); });By extracting those code snippets into their own separate factory functions, we can avoid scrolling fatigue, keep lifecycle links snappy, and make it easier to navigate the file and find what we're looking for.Not prioritizing the types of tests you runThis has more to do with large or huge codebases where there are literally hundreds or even thousands of test cases running every time a new set of commits wants to be merged into the codebase.Image descriptionIn such cases, running all the test suites can take literally hours, and you may not always have the time or resources to do so.When time or resources are limited, it's important to strategically choose the type of test to prioritize. Generally, integration tests provide better reliability assurances due to their broader scope. So when you have to choose between the two, it's often a good idea to choose integration tests over unit tests.Image descriptionUsing logic in your test casesWe want to avoid logic in our test cases whenever possible.Test cases should only have simple validation and avoid things like try-catch blocks or if-else conditionals.This keeps your tests clean and focused only on the expected flow because it makes the tests easier to understand at a glance.The only exception is when you're writing helper or factory functions that set up scenarios for tests.Using loose validations instead of strict assertionsThis is usually a sign that you might need to refactor the piece of code you're testing or that you need to make a minor adjustment to your mocks.For example, instead of checking if the value is greater than 1, you should be more specific and assert that the value is 2.Or, if you're checking data for a User object, you should assert that each piece of data is exactly as you expect, rather than just checking for an ID match.Loose checks can mask edge cases that could fail in the future.Improper Implementation of Mock BehaviorThis one is hard to find and that's why you can find an example in every codebase.It's one of the sneakiest but common testing issues and it's hard to notice at first glance.It can happen when the mock behavior is overly simplified or when it doesn't accurately reflect edge cases and error conditions.As a result, tests may pass, but they will not provide a reliable indication of how the system will perform under various conditions, resulting in future errors and unexpected problems, and test cases with simulated behavior that end up doing more harm than good.I hope this post helps you identify those practices that we should avoid when testing.Profile@khriztianmoren

Testing framework - Node.js

2020-04-17
javascripttestingnodejs

Once an application is running in production, we might be afraid to make changes. How do we know that a new feature, a fix, or a refactor won't break existing functionality?We can manually use our application to try to find bugs, but without maintaining an exhaustive checklist, it's unlikely we'll cover all possible failure points. And honestly, even if we did, it would take too long to run our entire application after every commit.By using a testing framework, we can write code that verifies our previous code still works. This allows us to make changes without fear of breaking expected functionality.But there are many different testing frameworks, and it can be difficult to know which one to use. Below, I will talk about three of them for Node.js:TapeAvaJestTAPEThis derives its name from its ability to provide structured results through TAP (Test Anything Protocol). The output of our runner is human-friendly, but other programs and applications cannot easily parse it. Using a standard protocol allows for better interoperability with other systems.Additionally, Tape has several convenience methods that allow us to skip and isolate specific tests, as well as verify additional expectations such as errors, deep equality, and throwing.Overall, the advantage of Tape is its simplicity and speed. It is a solid and straightforward harness that gets the job done without a steep learning curve.Here is what a basic test with Tape looks like:const test = require("tape"); test("timing test", (t) => { t.plan(2); t.equal(typeof Date.now, "function"); const start = Date.now(); setTimeout(function () { t.equal(Date.now() - start, 100); }, 100); });And if we run it, it looks like this:$ node example/timing.js TAP version 13 # timing test ok 1 should be strictly equal not ok 2 should be strictly equal --- operator: equal expected: 100 actual: 107 ... 1..2 # tests 2 # pass 1 # fail 1The test() method expects two arguments: the name of the test and the test function. The test function has the t object as an argument, and this object has methods we can use for assertions: t.ok(), t.notOk(), t.equal(), and t.deepEqual() to name a few.AVAAVA has a concise API, detailed error output, embraces new language features, and has process isolation to run tests in parallel. AVA is inspired by Tape's syntax and supports reporting through TAP, but it was developed to be more opinionated, provide more features, and run tests concurrently.AVA will only run tests with the ava binary. With Tape, we could run node my-tape-test.js, but with AVA we must first ensure that AVA is installed globally and available on the command line (e.g., npm i -g ava).Additionally, AVA is strict about how test files are named and will not run unless the file ends with "test.js".One thing to know about AVA is that by default it runs tests in parallel. This can speed up many tests, but it is not ideal in all situations. When tests that read and write to the database run simultaneously, they can affect each other.AVA also has some helpful features that make setup and teardown easier: test.before() and test.after() methods for setup and cleanup.AVA also has test.beforeEach() and test.afterEach() methods that run before or after each test. If we had to add more database tests, we could clear our database here instead of individual tests.Here is what an AVA test looks like:const test = require("ava"); test("foo", (t) => { t.pass(); }); test("bar", async (t) => { const bar = Promise.resolve("bar"); t.is(await bar, "bar"); });When iterating on tests, it can be useful to run AVA in "watch mode". This will watch your files for changes and automatically rerun the tests. This works particularly well when we first create a failing test. We can focus on adding functionality without having to keep switching to restart the tests.AVA is very popular and it's easy to see why. AVA is an excellent choice if we are looking for something that makes it easy to run tests concurrently, provides helpers like before() and afterEach(), and provides better performance by default, all while maintaining a concise and easy-to-understand API.JestIt is a testing framework that has grown in popularity alongside React.js. The React documentation lists it as the recommended way to test React, as it allows using jsdom to easily simulate a browser environment. It also provides features to help mock modules and timers.Although Jest is very popular, it is mainly used for front-end testing. It uses Node.js to run, so it is capable of testing both browser-based code and Node.js applications and modules. However, keep in mind that using Jest to test Node.js server-side applications comes with caveats and additional configuration.Overall, Jest has many features that can be attractive. Here are some key differences from Tape and AVA:Jest does not behave like a normal Node.js module.The test file must be run with jest, and several functions are automatically added to the global scope (e.g., describe(), test(), beforeAll(), and expect()). This makes test files "special" as they do not follow the Node.js convention of using require() to load jest functionality. This will cause issues with linters like standard that restrict the use of undefined globals.Jest uses its global expect() to perform checks, instead of standard assertions. Jest expects it to read more like English. For example, instead of doing something like t.equal(actual, expected, comment) with tape and AVA, we use expect(actual).toBe(expected). Jest also has smart modifiers that you can include in the chain like .not() (e.g., expect(actual).not.toBe(unexpected)).Jest has the ability to mock functions and modules. This can be useful in situations where it is difficult to write or change the code we are testing to avoid slow or unpredictable results in a test environment. An example in the Jest documentation is preventing axios from making a real HTTP request to an external server and instead returning a preconfigured response.Jest has a much larger API and many more configuration options. Some of them do not work well when testing for Node.js. The most important option we need to set is that testEnvironment should be "node". If we do not do this, jest uses the default configuration where our tests will run in a browser-like environment using jsdom.Here is what a Jest test looks like:const sum = require("./sum"); test("adds 1 + 2 to equal 3", () => { expect(sum(1, 2)).toBe(3); });Jest has a much larger API and offers more functionality than AVA or tape. However, the larger scope is not without drawbacks. When using Jest to test Node.js code, we have to:Agree to use undefined globals.Not use functions like mocked timers that interfere with packages like Mongoose.Configure the environment correctly so it does not run in a simulated browser by default.Consider that some code may run 20-30 times slower in Jest compared to other test runners.Many teams will choose Jest because they are already using it on the front-end and do not like the idea of having multiple test runners, or they like the built-in features like mocks and do not want to incorporate additional modules. Ultimately, these trade-offs must be made on a case-by-case basis.Other testing toolsThere are plenty of other testing tools like Istanbul, nyc, nock, and replay that we do not have space to go into here.I hope this has been helpful and/or taught you something new!Profile@khriztianmoreno ďż˝