首页 文章

使用React / Redux进行酶测试 - 使用setState进行浅层渲染问题

提问于
浏览
0

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

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

  • 组件需要接收"response"道具(任何字符串都可以)

  • 组件需要将其初始"started"本地状态更新为true . 这是通过实际组件中的按钮完成的 .

这会让测试产生一些令人头疼的问题 . 以下是确定要渲染内容的实际行:

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)更新本地状态(需要挂载,而不是浅层),I不能让一切都工作 .

我已经看过关于这个话题的聊天,看起来这是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.

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

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

    如何导入连接的组件 ScoreQuiz

    我看到您已经正确导出 Quiz 组件并默认导出已连接的 Quiz 组件 .

    尝试导入

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

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

  • 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 中的某些组件引起的问题 .

  • 0

    您收到该错误的原因是您尝试通过调用 connect()() 来测试生成的包装器组件 . 该包装器组件希望能够访问Redux存储 . 通常,该商店可用作context.store,因为在组件层次结构的顶部,您将拥有 <Provider store={myStore} /> . 但是,你输了一个错误 .

    此外,如果您尝试测试组件内的组件,可能是完整的DOM渲染器可能是解决方案 .

    如果您需要强制更新组件,Enzyme会让您回来 . 它提供了 update() ,并且如果在对组件的引用上调用 update() ,该组件将强制组件重新呈现自身 .

相关问题