RxJs is cool when you work with complex async operations. RxJS is designed for reactive programming using Observables. It converts your async operations to Observables. With observables we can "watch" the data stream, passively listening for an event. React hooks supercharge your functional components in many ways. With hooks, we can abstract and decouple the logics with custom hooks. With the separation of logics makes your code testable and share between components. This post helps explain how thou can test hook that uses RxJs inside to listen to mouse click and delay the click with RxJs's operator. useEffect debounceTime Hooks that we are using here. Enhance functional component with the state. useState: We can perform DOM manipulation and select. useEffect: RxJs Operators We are using here. returns Observable value from the provided function using emitted by the source. map: Emits a value from the source Observable only after a particular time has passed without another source emission. debouonceTime: Before we jump to write our test code, let see our example component. Button.tsx React, { SFC} {useClick} Props = { interval?: ; label?: ; } Button:SFC<Props> = { {ref, count} = useClick(props.interval) <button data-testid= ref={ref}>Hello {count}< import from 'react' import from './useClick' type number string const ( ) => props:Props const return "btn" /button> } export default Button useClick.ts React, { useRef, useEffect, useCallback, useState, RefObject, Dispatch} {fromEvent, Observable, Subscribable, Unsubscribable} {map, debounceTime} type NullableObservarbel = Observable<any> | ; type NUllabe = HTMLButtonElement | ; type NullableSubscribable = Subscribable<any> | type NullableUnsubscribable = Unsubscribable | type Result = { : RefObject<HTMLButtonElement>; count:number; updateCount:Dispatch<React.SetStateAction<number>>; } isString = (input:any): ( input === && input !== ) makeObservable = (el:NUllabe, :string): el HTMLElement && isString(eventType) ? fromEvent(el, eventType) : useClick = (time:number = ): { button: RefObject<HTMLButtonElement> = useRef( ) [count, updateCount] = useState<number>( ) fireAfterSubscribe = useCallback( {updateCount(c)}, []) useEffect( { el = button.current observerble = makeObservable(el, ) _count = count subscribable:NullableSubscribable = subscribe:NullableUnsubscribable = (observerble){ subscribable = observerble.pipe( map( _count++), debounceTime(time) ) subscribe = subscribable.subscribe(fireAfterSubscribe) } subscribe && subscribe.unsubscribe() }, []) { :button, count, :fireAfterSubscribe} } // useClick.ts import from 'react' import from 'rxjs' import from 'rxjs/operators' null null null null export ref export const => Boolean typeof "string" "" export const eventType => NullableObservarbel instanceof null export const 500 => Result const null const 0 const ( ) => c : => () () => void const const 'click' let let null let null if => e return => () // cleanup subscription // eslint-disable-next-line return ref updateCount Above example, we have 2 files. is an ordinary button component. Button.tsx: contains the custom hook called and . functions. useClick.ts: useSubscriber makeObservable Button uses to delay the button clicks. Each clicks debounced with RxJs function. useSubscriber debounceTime Clicks will be ignored while the user clicks within 400ms. Once the user has done clicks, it waits 400ms then fire the last event. Simple!.🤓 Now lets test! 🧪. Let's start with something simple. Test the `useState` hook. React {useClick} describe( , () => { it( , () => { result = useClick( ) {updateCount} = result updateCount( ) expect(result.current.count).toBe( ) }) }) // useClick.test.tsx - v1 import from 'react' import from './useClick' 'useState' 'should update count using useState' const 400 // test will break due to invarient violation const 8 8 Now run `yarn test.` Invariant Violation: Invalid hook call. Hooks can only be called inside of the body of a function component.... Not the result that we expected. The error above means that calling hooks outside the functional component body is Invalid. In this case, we can use react hooks testing utility library . @testing-library/react-hooks { renderHook } import from '@testing-library/react-hooks With we can call the hooks of the body of a function component. renderHook outside let’s just replace with const result = useSubscriber(400) const {result} = renderHook(() => useSubscriber(400) also, with const {updateCount} = result const {updateCount} = result.current Then wrap your setState call with otherwise your test throws an error. act React { useClick } { renderHook, act hookAct } describe( , () => { it( , () => { {result} = renderHook( useClick( )) {updateCount} = result.current hookAct( { updateCount( ) }) expect(result.current.count).toBe( ) }) }) // useClick.test.tsx -v2 import from 'react' import from './useClick' import as from '@testing-library/react-hooks' 'useState' 'should update count using useState' const => () 400 const => () 8 8 Okay, now we good to go. Again run . yarn test Voila!. As expected. More tests Now we test function. Function take and event type as a string and should return Observable. It should return false if given an invalid argument(s). makeObservable makeObservable DOMElement Lets test with no arguments, invalid arguments, and correct arguments. import {render, fireEvent} from '@testing-library/react' React { makeObservable, useClick } {Observable} Button { render } { renderHook, act hookAct } describe( , { it( , { {result} = renderHook( useClick( )) {updateCount} = result.current hookAct( { updateCount( ) }) expect(result.current.count).toBe( ) }) }) describe( , { it( , { observable = makeObservable({}, ) expect(observable Observable).toBe( ) }) it( , { {getByTestId} = render(<Button/>) el = getByTestId( ) HTMLButtonElement observable = makeObservable(el, ) expect(observable Observable).toBe( ) }) it( , { observable = makeObservable( , ) expect(observable Observable).toBe( ) }) it( , { {getByTestId} = render(<Button/>) el = getByTestId( ) HTMLButtonElement observable = makeObservable(el, ) expect(observable Observable).toBe( ) }) }) import from 'react' import from './useClick' import from 'rxjs' import from './Button' import from '@testing-library/react' import as from '@testing-library/react-hooks' 'useState' => () 'should update count using useState' => () const => () 400 const => () 8 8 'makeObservable' => () 'should return false for non HTMLElement' => () const 'click' instanceof false 'should return false for non non string event' => () const const 'btn' as const 20 instanceof false 'should return false for null' => () const null 'click' instanceof false 'should create observable' => () const const 'btn' as const 'click' instanceof true Now the last test. Test Subscriber and useEffect. Testing useEffect and observable is the complicated part. Because and makes your component render asynchronous. useEffect Assertions that inside the subscribers never run and the test pass. To capture 's side effect, we can wrap our test code with from useEffect act react-dom/test-utils. To run assertions inside the subscription, we can use `done().` Jest wait until the done callback is called before finishing the test. React {isString, makeObservable, useClick } {Observable} {map, debounceTime} Button { render, fireEvent, waitForElement } {act} { renderHook, act hookAct } describe( , () => { it( , () => { {result} = renderHook( useClick( )) {updateCount} = result.current hookAct( { updateCount( ) }) expect(result.current.count).toBe( ) }) }) describe( , () => { it( , () => { observable = makeObservable({}, ) expect(observable Observable).toBe( ) }) it( , () => { {getByTestId} = render( ) el = getByTestId( ) HTMLButtonElement observable = makeObservable(el, ) expect(observable Observable).toBe( ) }) it( , () => { observable = makeObservable( , ) expect(observable Observable).toBe( ) }) it( , () => { {getByTestId} = render( ) el = getByTestId( ) HTMLButtonElement observable = makeObservable(el, ) expect(observable Observable).toBe( ) }) }) describe( , () => { it( , () => { expect(isString( )).toEqual( ) }) it( , () => { expect(isString({})).toEqual( ) }) it( , () => { expect(isString( )).toEqual( ) }) it( , () => { expect(isString( )).toEqual( ) }) }) describe( , () => { it( , (done) => { act( () => { {getByTestId} = render( ) el = waitForElement( getByTestId( )) HTMLButtonElement observerble = makeObservable(el, ); (observerble){ count = observerble .pipe( map( count++), debounceTime( ) ) .subscribe( { expect(s).toEqual( ) done() }) fireEvent.click(el) fireEvent.click(el) fireEvent.click(el) fireEvent.click(el) fireEvent.click(el) fireEvent.click(el) } }) }) }) // useClick.test.tsx import from 'react' import from './useClick' import from 'rxjs' import from 'rxjs/operators' import from './Button' import from '@testing-library/react' import from 'react-dom/test-utils' import as from '@testing-library/react-hooks' 'useState' 'should update count using useState' const => () 400 const => () 8 8 'makeObservable' 'should return false for non HTMLElement' const 'click' instanceof false 'should return false for non non string event' const < /> Button const 'btn' as const 20 instanceof false 'should return false for null' const null 'click' instanceof false 'should create observable' const < /> Button const 'btn' as const 'click' instanceof true 'isString' 'is a string "click"' 'click' true 'is not a string: object' false 'is not a string: 9' 9 false 'is not a string: nothing' null false 'Observable' 'Should subscribe observable' async await async const < /> Button const await => () 'btn' as const 'click' if let 1 => e 400 => s 6 And button component test React ReactDOM Button { render, fireEvent, waitForElement, waitForDomChange } describe( , { it( , { div = .createElement( ); ReactDOM.render(<Button />, div); ReactDOM.unmountComponentAtNode(div); }); }) describe( , { it( , (done) => { {getByTestId} = render(<Button interval={ }/>) el = waitForElement( getByTestId( )) HTMLButtonElement fireEvent.click(el) fireEvent.click(el) fireEvent.click(el) t = waitForDomChange({container: el}) expect(el.textContent).toEqual( ) done() }) }) // Button.test.tsx import from 'react' import from 'react-dom' import from './Button' import from '@testing-library/react' 'Button component' => () 'renders without crashing' => () const document 'div' 'Dom updates' => () 'should update button label to "Hello 2"' async const 500 const await => () 'btn' as const await 'Hello 2' Now run yarn test. Now everything runs as expected, and you can see code coverage results and its more than 90%. In this post, we've seen how to write tests for React Hooks that RxJS observable that's inside the custom hook with the react-testing-library. If you have any questions or comments, you can share them below.