نوشتن تست در ReactJs برای هوک ها

داخل یکی از مقاله های قبلیم در مورد نوشتنن تست برای Reactjs صحبت کردیم .

https://virgool.io/@nima.2004hkh/%D9%86%D9%88%D8%B4%D8%AA%D9%86-%D8%AA%D8%B3%D8%AA-%D8%AF%D8%B1-reatcjs-%D8%A8%D9%87-%DA%A9%D9%85%DA%A9-jest-%D9%88-enzyme-uzkagtsqm5uc


مشکلی که داشتم چی بود؟ اینکه من میخواستم داخل یک functional component که هوک داره ، یک تست بنویسم . من داخل کامپوننت هم از Material-ui ‌استفاده کردم چون کار باهاش برام لذت بخشه . ( سلیقه ای دیگه . منم بد سلیقم )‌.

هرکار میکردم نمیتونستم به سلکتورها برسم . به هر دری زدم و متوجه شدم که برای هوک ها باید کارهای دیگه ای کرد و به شکل دیگه نوشت . موضوع اینه که وقتی ما Functional Component داریم مینویسیم ، Enzyme نمیتونه کامپوننت رو رندر کنه . چون باید حتما کامپوننت یک instance از کلاس باشه . اما ما Function داریم مینویسیم .

بعد از کلی زیر و رو کردن فهمیدم باید از یک کتابخانه دیگه استفاده کنیم .

کتابخانه react-testing-library همونیه که میتونه کمک کنه به ما .

حالا با یک مثال ساده نگاه میندازیم به موضوع :

import React, {useState} from 'react'
import {withStyles} from '@material-ui/core/styles';
import PropTypes from 'prop-types';
import InputAdornment from '@material-ui/core/InputAdornment';
import TextField from '@material-ui/core/TextField';
import classNames from 'classnames';
import Grid from "@material-ui/core/Grid";
import {isNumber} from "persian-regex";
import Fab from '@material-ui/core/Fab';

const styles = theme => ({
    root: {
        display: 'flex',
        flexWrap: 'wrap',
    },
    margin: {
        margin: theme.spacing.unit,
        height: 39
    },
    textField: {
        flexBasis: 200,
    },
    rootGrid: {
        padding: 0,
        height: '100%',
        overflow: 'hidden'
    },
    rootGridItem: {
        textAlign: 'left'
    },
    adornment: {
        '& p':
            {
                color: '#ffffff',
                fontSize: 12,
                display: 'inherit',
                width: 85,
                paddingRight: 5
            }
    },
    inputLabel: {
        color: '#ffffff'
    },
    extendedIcon: {
        marginRight: theme.spacing.unit,
    },
    fabButton: {
        width: '100%',
        margin: '0 auto',
        textAlign: 'center',
        borderRadius: 5
    },
    fabButtons: {
        padding: 6
    },
    buyFab: {
        backgroundColor: '#12b886',
        color: '#ffffff'
    },
    sellFab: {
        backgroundColor: '#fa5252',
        color: '#ffffff'
    }
});

function Limit(props) {
    const [buyAmount, setBuyAmount] = useState(0);
    const [alert, setAlert] = useState("");
    
    function handleBuy() {
        setAlert("")
        if (buyAmount === 0 || buyAmount === "")
            setAlert("missing inputs")
        setTimeout(() => {
            setAlert("");
        }, 2000)
    }

    function handleChange(event, name) {
        const amount = event.target.value;
        if (amount !== undefined)
            if (isNumber(amount))
                switch (name) {
                    case 'buyAmount':
                        setBuyAmount(amount);
                        break;
                }
    }

    const {classes} = props;

    return (
        <React.Fragment>
            <Grid container classes={{container: classes.rootGrid}}>

                <Grid item xs={6} className={classes.rootGridItem}>
                    <TextField
                        id="filled-adornment-amount"
                        className={classNames(classes.margin, classes.textField)}
                        variant="filled"
                        InputLabelProps={{
                            classes: {root: classes.inputLabel}
                        }}

                        value={buyAmount > 0 ? buyAmount : ""}
                        ={(e) => handleChange(e, 'buyAmount')}
                        InputProps={{
                            inputProps: {'data-testid': "buyAmount"},
                            classes: {root: classes.inputLabel},
                            startAdornment: <InputAdornment position="start" classes={{root: classes.adornment}}>
                                Amount (IRR)
                            </InputAdornment>,
                        }}
                    />
                </Grid>

                <Grid item xs={6} className={classNames(classes.rootGridItem, classes.fabButtons)}>
                    <Fab
                        data-testid="buy"
                        classes={{
                            root: classes.buyFab
                        }}
                        ={handleBuy}
                        variant="extended"
                        size="large"
                        color="primary"
                        aria-label="Buy"
                        className={classes.fabButton}
                    >
                        Buy
                    </Fab>
                </Grid>
                <div data-testid="alert">{alert}</div>

            </Grid>

        </React.Fragment>
    )
}
Limit.propTypes = {
    classes: PropTypes.object.isRequired,
};
export default withStyles(styles)(Limit);

داخل این مثال از material-ui و یکی از پکیج هایی که خودم نوشتم به اسم persian-regex استفاده کردم .

حالا میخوام تستم رو درست کنم . میرم داخل پوشه __tests__ و یک فایل به اسم Limit.js ‌میسازم . تست ما از این پوشه پیروی میکنه .

داخل ترمینال پکیجمون رو نصب میکنیم :

npm i react-testing-library --save-dev

حالا داخل Limit.js که ساختیم برای تست کد زیر رو میزنیم :

import React from 'react';
import Limit from "../Components/Limit"; // limit component we made above
import 'react-testing-library/cleanup-after-each'
import {render, fireEvent} from 'react-testing-library'
import {isNumber} from "persian-regex";

describe('Test Limit Component', () => {

    describe('Click on Buy Buttons and check validation', () => {

        describe('should click on Buy Button ', () => {
            it('should return false , cause inputs are empty', () => {
                const {getByTestId} = render(<Limit/>)
                const buyButton = getByTestId('buy');
                const buyAmountInput = getByTestId('buyAmount');
                const alert = getByTestId('alert');

                fireEvent.change(buyAmountInput, {target: {buyAmount: ""}});
                fireEvent.click(buyButton)

                if (buyAmountInput.buyAmount === "") {
                    expect(alert).toContain('missing inputs')
                }
            })

            it('write into buy Amount input and it should get value : 123456789', () => {
                const {getByTestId} = render(<Limit/>)
                const number = '123456789';
                const buyAmountInput = getByTestId('buyAmount');
                fireEvent.change(buyAmountInput, {target: {buyAmount: isNumber(number) ? number : ''}})
                expect(buyAmountInput.buyAmount).toEqual(number)
            })

            it('write into buy Amount input and it should get empty value because input is not valid', () => {
                const {getByTestId} = render(<Limit/>)
                const number = 'hi there';
                const buyAmountInput = getByTestId('buyAmount');
                fireEvent.change(buyAmountInput, {target: {buyAmount: isNumber(number) ? number : ''}})
                expect(buyAmountInput.buyAmount).toEqual('')
            })

        })

    })
})

داخل کد بالا سه شرط نوشتم . یکی کلیک کردن روی دکمه خرید ، بدون وارد کردن مقدار ، یکی کلیک با مقدار درست یکی هم با مقدار اشتباه و validate کدن input . حالا بعد از اجرای دستور npm run test ، تست یکی یکی اجرا میشه و درست بودنشون چک میشه .