使用React / Redux进行酶测试 - 使用setState

时间:2018-03-26 21:42:40

标签: reactjs redux enzyme

我有一个React组件,如果它的初始本地状态发生了变化,它会加载另一个组件。我无法进行干净的测试,因为我需要设置本地状态和浅层渲染,以便在新组件安装时子组件不会崩溃,因为redux存储不在那里。似乎这两个目标在酶中是不相容的。

要显示子组件,需要进行以下操作:

  1. 该组件需要接收响应"道具(任何字符串都可以)
  2. 该组件需要首先启动"启动"本地州更新为真。这是通过实际组件中的按钮完成的。
  3. 这会让测试产生一些令人头疼的问题。以下是确定要渲染内容的实际行:

    let correctAnswer = this.props.response ? <div className="global-center"><h4 >{this.props.response}</h4><Score /></div> : <p className="quiz-p"><strong>QUESTION:</strong> {this.props.currentQuestion}</p>; 
    

    这是我目前的酶测试:

    it('displays score if response and usingQuiz prop give proper input', () => {
        const wrapper = shallow(<Quiz usingQuiz={true} answers={[]} response={'example'}/>);  
        wrapper.setState({ started: true }) 
        expect(wrapper.contains(<Score />)).toEqual(true)
    }); 
    

    我使用的是浅层,因为每当我使用mount时,我都会这样:

    Invariant Violation: Could not find "store" in either the context or props of "Connect(Score)". Either wrap the root component in a <Provider>, or explicitly pass "store" as a prop to "Connect(Score)".
    

    因为组件是通过父级显示的,所以我不能简单地选择断开连接的版本。使用浅似乎纠正了这个问题,但后来我无法更新本地状态。当我尝试这个时:

    it('displays score if response and usingQuiz prop give proper input', () => {
        const wrapper = shallow(<Quiz usingQuiz={true} answers={[]} response={'example'}/>);  
        wrapper.setState({ started: true })  
        expect(wrapper.contains(<Score />)).toEqual(true)
    }); 
    

    测试失败,因为浅层没有让DOM更新。

    我需要满足两个条件。我可以单独做每个条件,但是当我需要同时1)在组件内部渲染一个组件(需要浅或者会对不在那里的商店感到厌恶),以及2)更新本地状态(需要挂载,而不是浅层),我无法一次性完成所有工作。

    我已经看过关于这个话题的聊天,而且这似乎是Enzyme的合法限制,至少在2017年。这个问题是否已得到解决?测试这个非常困难。

    如果有人需要它作为参考,这是完整的组件:

    import React from 'react'; 
    import { connect } from 'react-redux'; 
    import { Redirect } from 'react-router-dom';
    import { Transition } from 'react-transition-group';  
    import { answerQuiz, deleteSession, getNewQuestion  } from '../../actions/quiz'; 
    import Score from '../Score/Score'; 
    import './Quiz.css'; 
    
    export class Quiz extends React.Component {
    
        constructor(props) {
            super(props); 
            // local state for local component changes
            this.state = {
                started: false
            }
        }
    
        handleStart() {
            this.setState({started: true})
        }
    
        handleClose() {
            this.props.dispatch(deleteSession(this.props.sessionId))
        }
    
        handleSubmit(event) {  
            event.preventDefault();
            if (this.props.correctAnswer && this.props.continue) {
                this.props.dispatch(getNewQuestion(this.props.title, this.props.sessionId)); 
            }
            else if (this.props.continue) {
                const { answer } = this.form; 
                this.props.dispatch(answerQuiz(this.props.title, answer.value, this.props.sessionId)); 
            }
            else {
                this.props.dispatch(deleteSession(this.props.sessionId))
            }
        }
    
        render() {  
    
            // Transition styles
            const duration = 300; 
            const defaultStyle = {
                opacity: 0, 
                backgroundColor: 'rgba(0, 0, 0, 0.7)',
                height: '100%', 
                width: '100%', 
                margin: '0px', 
                zIndex: 20, 
                top: '0px', 
                bottom: '0px', 
                left: '0px', 
                right: '0px', 
                position: 'fixed',
                display: 'flex', 
                alignItems: 'center', 
                transition: `opacity ${duration}ms ease-in-out`
            }
    
            const transitionStyles = {
                entering: { opacity: 0 }, 
                entered: { opacity: 1 }
            }
    
            // Response text colors
            const responseClasses = [];
            if (this.props.response) {
                if (this.props.response.includes("You're right!")) {
                    responseClasses.push('quiz-right-response')
                }
                else {
                    responseClasses.push('quiz-wrong-response');
                }
            }
    
            // Answer radio buttons
            let answers = this.props.answers.map((answer, idx) => (
                <div key={idx} className="quiz-question">
                    <input type="radio" name="answer" value={answer} /> <span className="quiz-question-label">{answer}</span>
                </div>
            )); 
    
            // Question or answer
            let correctAnswer = this.props.response ? <div className="global-center"><h4 className={responseClasses.join(' ')}>{this.props.response}</h4><Score /></div>: <p className="quiz-p"><strong>QUESTION:</strong> {this.props.currentQuestion}</p>; 
    
            // Submit or next 
            let button = this.props.correctAnswer ? <button className="quiz-button-submit">Next</button> : <button className="quiz-button-submit">Submit</button>; 
    
            if(!this.props.continue) {
                button = <button className="quiz-button-submit">End</button>
            }
    
            // content - is quiz started? 
            let content; 
            if(this.state.started) {
                content = <div>
                    <h2 className="quiz-title">{this.props.title} Quiz</h2>
                        { correctAnswer }
                        <form className="quiz-form" onSubmit={e => this.handleSubmit(e)} ref={form => this.form = form}>
                            { answers }
                            { button }
                        </form>
                    </div>
            } else {
                content = <div>
                    <h2 className="quiz-title">{this.props.title} Quiz</h2>
                    <p className="quiz-p">So you think you know about {this.props.title}? This quiz contains {this.props.quizLength} questions that will test your knowledge.<br /><br />
                    Good luck!</p>
                     <button className="quiz-button-start" onClick={() => this.handleStart()}>Start</button>
                </div>
            }
    
            // Is quiz activated? 
            if (this.props.usingQuiz) {
                return ( 
                    <Transition in={true} timeout={duration} appear={true}>
                        {(state) => (
                                <div style={{ 
                                    ...defaultStyle,
                                    ...transitionStyles[state]
                        }}>
                        {/* <div className="quiz-backdrop"> */}
                            <div className="quiz-main">
                                <div className="quiz-close" onClick={() => this.handleClose()}>
                                    <i className="fas fa-times quiz-close-icon"></i>
                                </div>
                                { content } 
                            </div>
                        </div>
                    )}
                    </Transition >
                )  
            } 
            else {
                return <Redirect to="/" />; 
            }      
        }
    }
    
    const mapStateToProps = state => ({
        usingQuiz: state.currentQuestion, 
        answers: state.answers, 
        currentQuestion: state.currentQuestion, 
        title: state.currentQuiz,
        sessionId: state.sessionId,  
        correctAnswer: state.correctAnswer, 
        response: state.response,
        continue: state.continue, 
        quizLength: state.quizLength,
        score: state.score, 
        currentIndex: state.currentIndex
    }); 
    
    export default connect(mapStateToProps)(Quiz); 
    

    这是我使用mount的测试(由于缺少存储导致崩溃):

    import React from 'react'; 
    import { Quiz } from '../components/Quiz/Quiz';
    import { Score } from '../components/Score/Score';  
    import { shallow, mount } from 'enzyme'; 
    
        it('displays score if response and usingQuiz prop give proper input', () => {
            const wrapper = mount(<Quiz usingQuiz={true} answers={[]} response={'example'}/>);  
            wrapper.setState({ started: true })  
            expect(wrapper.contains(<Score />)).toEqual(true)
        }); 
    });
    

3 个答案:

答案 0 :(得分:0)

这看起来像应该使用mount(..)进行测试的组件。

如何导入已连接的组件ScoreQuiz

我发现您已正确导出Quiz组件并默认导出已连接的Quiz组件。

尝试使用

导入
import { Score } from '../Score/Score';
import { Quiz } from '../Quiz/Quiz';

在您的测试中,mount(..)。如果从默认导出导入,您将获得导入的连接组件,我认为这是导致错误的原因。

答案 1 :(得分:0)

您确定Transition组件是否允许其显示内容?我使用这个组件,无法在测试中正确处理它... 您是否可以通过以下方式更改render s return

if (this.props.usingQuiz) {
  return (
    <div>
      {
        this.state.started && this.props.response ?
        (<Score />) :
        (<p>No score</p>)
      }
    </div>
  )
}

你的测试看起来像这样:

it('displays score if response and usingQuiz prop give proper input',() => {
  const wrapper = shallow(<Quiz usingQuiz={true} answers={[]} response={'example'}/>);
  expect(wrapper.find('p').text()).toBe('No score');
  wrapper.setState({ started: true });
  expect(wrapper.contains(<Score />)).toEqual(true);
});

我还测试了shallow s setState,这样的小测试效果很好:

test('HeaderComponent properly opens login popup', () => {
  const wrapper = shallow(<HeaderComponent />);
  expect(wrapper.find('.search-btn').text()).toBe('');
  wrapper.setState({ activeSearchModal: true });
  expect(wrapper.find('.search-btn').text()).toBe('Cancel');
});

因此,我认为浅层正确处理setState以及render内某些组件导致的问题。

答案 2 :(得分:0)

出现该错误的原因是,您正在尝试测试通过调用connect()()生成的包装器组件。该包装器组件希望可以访问Redux存储。通常,该存储可以作为context.store使用,因为在组件层次结构的顶部,您将有一个<Provider store={myStore} />。但是,您将自己渲染连接的组件,而没有存储,因此会引发错误。

此外,如果您要测试组件内部的组件,则可能需要完整的DOM渲染器。

如果您需要强制更新组件,则酶可以帮您。它提供update(),如果您在对组件的引用上调用update(),将强制该组件重新呈现自身。