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

问题描述 投票:0回答:3

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

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

  1. 组件需要接收“响应”道具(任何字符串都可以)
  2. 组件需要将其初始“已启动”的本地状态更新为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)更新本地状态(需要挂载,而不是浅层),我不能让一切都工作。

我已经看过关于这个话题的聊天,看起来这是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)
    }); 
});
reactjs redux enzyme
3个回答
0
投票

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

你是如何导入连接组件ScoreQuiz的?

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

尝试导入

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

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


0
投票

你确定Transition组件让它的内容显示出来吗?我使用这个组件并且无法在测试中正确处理它...你可以用于测试目的改变你的renders 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);
});

我也测试了shallows 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(),它会强制组件重新渲染自己。

© www.soinside.com 2019 - 2024. All rights reserved.