0

I wanted to test a React class component function that returns JSX content. Below is my code:

Products component

export default class Products extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      products: [],
    };
  }

  iconRenderer = (data) => {
    return (
      <i
        className="fa fa-qrcode fa-3x"
      >
      </i>
    );
  };

  getDisplayColumns = () => {
    return [
      {
        fieldName: 'producticon',
        displayText: 'Name',
        isEditable: false,
        visible: true,
        columnSize: 2,
        renderer: this.iconRenderer
      }
    ];
  };

  render() {
    const displayColumns = this.getDisplayColumns();
    return (
      <div className=''>
            {this.state.products && this.state.products.length > 0 &&
            <CustomTableGeneric
              tableId="item-list-table"
              data={this.state.products}
              columns={displayColumns}
            >
            </CustomTableGeneric>
            }
          </div>
    );
  }
}

CustomTableGeneric component (I tried to simplify the code)

export default class CustomTableGeneric extends React.Component {
    constructor(props) {
        super(props);

        this.state = {
            currentTableData: [],
            columnsToDisplay: [],
        };

        this.renderRows = this.renderRows.bind(this);
        this.renderIndividualRow = this.renderIndividualRow.bind(this);
    }

    renderIndividualRow(data, dataKeys) {
        return dataKeys.map((item, index) => {
            let currentRowId = data['id'];
            let columnWidth = this.state.columnsToDisplay[index].columnSize;
            if (item.renderer) {
                    return (
                        <Col md={columnWidth} className="table-column-cell"
                             key={index}>
                            {item.renderer(data, item.fieldName)}
                        </Col>
                    );
                } else {
                    return (
                        <Col md={columnWidth} className="table-column-cell" key={index}>{data[item.fieldName]}</Col>
                    );
                }
        });
    }

    renderRows() {
        let dataKeys = clonedeep(this.state.columnsToDisplay);
        let dataRows = clonedeep(this.state.currentTableData);
        if (dataRows.length > 0) {
            return dataRows.map((row, index) => {
                return (
                        <Row key={index} className="table-row">
                            {this.renderIndividualRow(row, dataKeys)}
                        </Row>
                    );
            });
        }
    }

    render() {
        return (
            <Row id={this.props.tableId}>
                <Col className="custom-table">
                    <Row className="table-header">
                        {this.renderHeaders()}
                    </Row>
                    {this.renderRows()}
                </Col>
            </Row>
        );
    }
}

CustomTableGeneric.propTypes = {
    tableId: PropTypes.string,
    data: PropTypes.arrayOf(PropTypes.object).isRequired,
    columns: PropTypes.arrayOf(PropTypes.shape({
        fieldName: PropTypes.string,
        displayText: PropTypes.string,
        renderer: PropTypes.func,
    })).isRequired,
};

Products.test.js

import React from 'react';
import {shallow, mount} from 'enzyme';

import CustomTableGeneric from '../../components/CustomTableGeneric';
import Products from './Products';

const props = {
  id: '123'
};

describe('Products function tests', () => {
  it('should call the iconRenderer function', () => {
    const wrapper = shallow(<Products {...props} />);
    const result = wrapper
        .instance()
        .iconRenderer();
    console.log(result);

  });
});

and below is the console output when I am running the test.

{ '$$typeof': Symbol(react.element),
      type: 'i',
      key: null,
      ref: null,
      props: { className: 'fa fa-qrcode fa-3x' },
      _owner: null,
      _store: {} }

As you can see, if I call iconRenderer() explicitly from the test, it is executing. But what I am trying to test is to check whether iconRenderer() is getting called when the Products component is rendered. Please see how I am calling it inside render function, e.g. Products render() -> getDisplayColumns() -> CustomTableGeneric() -> iconRenderer().

Below is the actual test that I want to run

describe('Products function tests', () => {
  it('should call the iconRenderer function', () => {
    const wrapper = shallow(<Products {...props} />);
    jest.spyOn(wrapper.instance(), 'iconRenderer');
    expect(wrapper.instance().iconRenderer()).toHaveBeenCalled();
  });
});

but I am getting below error

Error: expect(jest.fn())[.not].toHaveBeenCalled()

jest.fn() value must be a mock function or spy.
Received:
  object: <i className="fa fa-qrcode fa-3x" />

Any suggestion is highly appreciated.

vpv
  • 920
  • 2
  • 20
  • 46
  • Did you tried using mount? – Muhammad Haseeb Mar 26 '20 at 05:57
  • yes, but received same error – vpv Mar 26 '20 at 06:06
  • Testing implementation details are not recommended,. however, you should test your component, if its there in the UI or not. – Muhammad Haseeb Mar 26 '20 at 06:17
  • I have separate tests for that, trying to find out if there is any way to check whether the function is called or not. – vpv Mar 26 '20 at 07:33
  • 1
    Its quite obvious that its been called that why it get rendered. we don't need to test implementation details like setState, Life cycle methods etc. For testing its more important to identify what needs to be test. Generally we should test UI interactions and business logic. – Muhammad Haseeb Mar 26 '20 at 07:53

1 Answers1

0

As I see it, your code has two problems:

  1. You are inadvertently invoking the mocked function in expect, which causes the error: expect is receiving the return value of mocked iconRenderer, rather than the mock itself. Change your expect line to:

    expect(wrapper.instance().iconRenderer).toHaveBeenCalled();
    
  2. You need to mock iconRenderer before rendering Product, so it's called when Product renders. So, it needs to be mocked before this line:

    const wrapper = shallow(<Products {...props} />);
    

    You can see an example of how to do that in this question.

Remolten
  • 2,614
  • 2
  • 25
  • 29
  • Hi @Remolten thanks for your answer. However, I have couple of questions. My **iconRenderer** function just returns html. In that case, why I need to mock this? Why can't I use the actual function as it is not doing anything fancy? – vpv Mar 26 '20 at 21:02
  • It needs to be mocked so that you can run assertions on it with `expect`, like `toHaveBeenCalled`. – Remolten Mar 30 '20 at 16:36