Coder Social home page Coder Social logo

wrapito-vitest's Introduction

๐ŸŒฏ wrapito-vitest

Wrap you tests so that you can test both behaviour and components with less effort.

IMPORTANT

This version is only compatible with vitest.

๐ŸŽฏ Motivation

As we are more focused on user interactions than implementation details. In order to test all the user interactions that can be done at a certain point of our app, the upper the component we render in the test, the better.

๐Ÿ’ก The idea

As we test our app we will be in two different scenarios where:

  • We will need to test that the user interactions cause the proper side effects such as making http calls or refreshing the UI.
  • In case we have a components library, we will need to test these by passing the needed props and checking that it returns (renders) the expected result.

In general, if you want to test behaviour, you need to simulate external actions from user or from http responses. Most of the existing testing libraries give you control of the user actions and thats why we just ask you to set in the config what is the render function of your testing library. Unfortunately, there aren't so many options when it comes to manage http requests and responses in the tests. To give the mounted component context about which path is the current path where the app should be mounted, what props does the component receive, what http requests will respond with which results or where should the portal be mounted we have used the builder pattern that makes tests much more semantic.

๐Ÿ”ง Installing

Using npm:

$ npm install wrapito

๐Ÿ‘ฉโ€๐Ÿ’ป Basic usage

const MyComponent = () => <span>Just a component</span>

const myWrappedComponent = wrap(MyComponent).mount()

๐Ÿ‘ฃ Initial setup

Because ๐ŸŒฏ wrapito doesn't want to know anything about how the components are mounted in the project that uses it, we can specify how we will mount our components by passing the rendering/mounting function of our library of preference. This way we make wrapito a little bit more agnostic. For example setup.wrapito.js

import { render } from '@testing-library/react'
import { configure } from 'wrapito'

configure({
  mount: render,
})

and add the previous file in jest.config.json

  "setupFiles": [
    "<rootDir>/config/jest/setup.wrapito.js"
  ],

If one or more of your components use a react portal in any way, you will need to specify the id of the node where it will be added:

import { render } from '@testing-library/react'
import { configure } from 'wrapito'

configure({
  mount: render,
  portal: 'modal-root',
})

๐Ÿฐ Builder API

withMocks (Deprecated)

It has the same API than the withNetwork builder. The main difference between them is that withMocks will fail if a given request, done by the production code, is not set up in the responses object.

withNetwork

By using this feature you can configure the responses for your http requests. If your component is making a request that is not set up in the responses object, it will not be validated and it will return an empty response with code 200.

import { wrap } from 'wrapito'

const responses = {
  host: 'my-host',
  method: 'get',
  path: '/path/to/get/a/single/product/,
  responseBody: { id: 1, name: 'hummus' },
  status: 200,
  catchParams: true,
  delay: 500,
}

wrap(MyComponent)
  .withNetwork(responses)
  .mount()

You can specify the default host via configuration:

import { configure } from 'wrapito'

const { API_HOST, API_VERSION } = process.env
configure({ defaultHost: `${API_HOST}${API_VERSION}` })

In addition, wrapito defaults the method to 'get' and status to 200. This means one can use withNetwork like this:

import { wrap } from 'wrapito'

const responses = {
  path: '/path/to/get/a/single/product/,
  responseBody: { id: 1, name: 'hummus' },
}

wrap(MyComponent)
  .withNetwork(responses)
  .mount()

Now, you might need to mock several http responses at the same time and that's why you can also pass an array of responses instead if you wish:

import { wrap } from 'wrapito'

const responses = [
  {
    path: '/path/to/get/the/products/list/,
    responseBody: [
      { id: 1, name: 'hummus' },
      { id: 2, name: 'guacamole' },
    ]
  },
  {
    path: '/path/to/get/a/single/product/,
    responseBody: { id: 1, name: 'hummus' },
  },
]

wrap(MyComponent)
  .withNetwork(responses)
  .mount()

There might be cases where one request is called several times and we want it to return different responses. An example of this could be an app that shows a list of products that may be updated over time and for this propose the app has a refresh button that will request the list again in order to update its content.

Well, it can be solved by specifying the response as multiple using multipleResponse as follows:

const responses = {
  path: '/path/to/get/the/products/list/,
  multipleResponses: [
  {
    responseBody: [
      { id: 1, name: 'hummus' },
      { id: 2, name: 'guacamole' },
    ]
  },
  {
    responseBody: [
      { id: 1, name: 'hummus' },
      { id: 2, name: 'guacamole' },
      { id: 3, name: 'peanut butter' },
    ]
   },
  ],
}

multipleResponses receives an array of responses where one set the responseBody, status or headers for every response.

When multipleResponses is present, ๐ŸŒฏ wrapito will ignore the responseBody at the root of the mock and will return one response per request made at the same time that sets the returned response as hasBeenReturned, which means it can be returned again, until all the array of responses is returned. In that case an exception will be raised.

This behaviour differs from using a single response for a given request as single response for a given request will return the response no matter how many times the request is called.

atPath

When mounting the whole app, it will be done at the default route passing the default path. But a specific route might need to be mounted and for that reason a path to match that route should be pass here.

import { wrap } from 'wrapito'

wrap(MyComponent)
  .atPath(`/the/path/to/match/a/route/that/mounts/my/component/3`)
  .mount()

By default it will use the native javascript history API, but you can provide a method to be called for change the app route with changeRoute:

import { configure } from 'wrapito'
import { history } from 'app.js'

configure({
  ..configuration,
  changeRoute: (route) => history.push(route)
})

withProps

Pass down the props to the wrapped component:

import { wrap } from 'wrapito'

const props = { message: 'MyComponent will receive this as prop' }

wrap(MyComponent).withProps(props).mount()

composing

As it is basically a builder, all the above functions can be used at the same time and these will be composed underneath:

import { wrap } from 'wrapito'

const props = { message: 'MyComponent will receive this as prop' }
const responses = {
  path: '/path/to/get/a/single/product/by/id/1/,
  responseBody: { id: 1, name: 'hummus' },
}

wrap(PreparationContainer)
  .atPath('/products/1')
  .withNetwork(responses)
  .withProps()
  .mount()

โœจ Utils

toHaveBeenFetched

Some times checking only the visual side effects in the UI it's not enough. In the case that we want to check if a particular network side effect is happening, this assertion will come in handy.

wrap(MyComponentMakingHttpCalls).withNetwork(responses).mount()

expect('/some/path').toHaveBeenFetched()

toHaveBeenFetchedWith

This assertion is an extension of toHaveBeenFetched but we will use it if we want to check the properties.

import { wrap, assertions } from 'wrapito'

wrap(MyComponentMakingHttpCalls).withNetwork(responses).mount()

expect('/some/path').toHaveBeenFetchedWith({
  method: 'POST',
  body: { key: 'value' },
})

toHaveBeenFetchedTimes

This assertion is to check how many times an API url is called.

import { wrap, assertions } from 'wrapito'

expect.extend(assertions)

const responses = [{ path: '/path/to/get/quantity/' }]

wrap(MyComponentMakingHttpCalls).withNetwork(responses).mount()

expect('/path/to/get/quantity/').toHaveBeenFetchedTimes(1)

๐Ÿ”ง Development

In order to test the library in a project without releasing the library:

  • npm run build
  • This will generate a local build in the dist folder
  • Copy the content of that folder in node_modules/wrapito in your project

Deploy new version in npm

You need to create a new tag for the project. E.g:

git tag v1.0.5
git push origin v1.0.5

This will run a workflow in github that will publish this version for you.

wrapito-vitest's People

Contributors

perizote avatar epalenque avatar sigfriedcub1990 avatar salvacarsi avatar alextorres94 avatar dependabot[bot] avatar dalfageme avatar juandiegombr avatar alfupe avatar pablogm95 avatar andreacasasrdrg avatar maurogestoso avatar oscarferrandiz-zz avatar anaurri avatar cem20903 avatar

Stargazers

 avatar

Watchers

Sergio Revilla avatar  avatar  avatar  avatar

wrapito-vitest's Issues

No type inference with extensions

๐Ÿ› Bug report

image

๐Ÿ‘ฃ Steps to reproduce

๐Ÿค” Expected behavior

It shouldn't yield a type error.

๐ŸŒฏ Wrapito version

latest

๐Ÿ’ฌ Comments

Ability to define default requests for every test

โœจ Feature proposal

Ability to define a set of default requests that will be used in every test.

๐ŸŽฏ Motivation

There are some scenarios where we would like to have a set of default requests for every test, i.e. Feature Flags.

๐Ÿ‘€ Example

import { render } from '@testing-library/react'

configure({
  mount: render,
  defaultResponses: [
     {
       path: '/feature_flags',
       responseBody: { 
         flags: ['flag_1', 'flag_2']
       }
     }
  ]
})

๐Ÿ’ฌ Comments

Better diffs

โœจ Feature proposal

Better diff when payloads don't match. Specifically, when using the .toHaveBeenFetchedWith matcher.

๐ŸŽฏ Motivation

To have a clear view of what went wrong when a given expectation didn't match.

๐Ÿ‘€ Example

๐Ÿ’ฌ Comments

Make wrapito agnostic of test framework

โœจ Feature proposal

I would like wrapito to be agnostic of the test framework we're using.

๐ŸŽฏ Motivation

Currently this version is only compatible with vitest because we use Vitest's mocking capabilities in order to verify if an endpoint was called, which parameters it was called with, etc.

Although this has worked well so far we shouldn't couple wrapito to any test framework, if possible. So, my suggestion here would be to use something like Tinyspy in order to do the verifications and decouple from vitest | jest.

This will also allow us to have only one version of wrapito.

๐Ÿ‘€ Example

This shouldn't affect end users since it's only used internally.

toHaveBeen* matchers not working

๐Ÿ› Bug report

toHaveBeenFetched, toHaveBeenFetchedWith and toHaveBeenFetchedTimes matchers are not working.

๐Ÿ‘ฃ Steps to reproduce

This reproducible using the library's Unit tests.

๐Ÿค” Expected behavior

To correctly determine if a path has been called or not.

๐ŸŒฏ Wrapito version

Latest

Add toHaveBeenNthFetchedWith

โœจ Feature proposal

Add toHaveBeenNthFetchedWith.

๐ŸŽฏ Motivation

We would like to assert that an endpoint has been called with a given payload at a certain point in time.

๐Ÿ‘€ Example

wrap(App)
	.withNetwork([
		path: '/users/1/update/',
		requestBody: { 
			name: 'Alexander',
			surname: 'Nevsky'
		},
	])
	.mount()

await startSomeFlow()
await updateUser() // Let's suppose that in this page we update the user data
await doSomethingElse()
await updateUserAgain() // At the end we update the user again

expect('/users/1/update/').toHaveBeenNthFetchedWith(1, {
	name: 'Alexander',
	surname: 'Nevsky',
}) // We want to assert that the first time this endpoint was called, the payload was this one.

๐Ÿ’ฌ Comments

We had a situation within acmofy-app that required this.

Ability to detect unused mocks

โœจ Feature proposal

Detect when some defined mocks are unused.

๐ŸŽฏ Motivation

Sometimes we copy/paste tests and forget to cleanup the mocks.

๐Ÿ‘€ Example

๐Ÿ’ฌ Comments

Extend vitest matchers auto-magically

โœจ Feature proposal

Auto-magically add matchers to vitest

๐ŸŽฏ Motivation

Currently, when we want to use toHaveBeenFetched and other matchers, we need to add them to a vitest.d.ts file and extend vitest there.

๐Ÿ‘€ Example

// setupTests.ts
import 'wrapito-vitest/integration' // This should automatically extend vitest & set correct type

๐Ÿ’ฌ Comments

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.