0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

getComputedStyle()がJestで期待通りに動作しない件について

Last updated at Posted at 2018-10-14

概要

Jestでテストを行った際にSVG要素の属性値を取得できなかったため、モックで対処した件の覚書。

ソースコード

App.js
import React from 'react';

class App extends React.Component {
  getWidthFromDiv() {
    const computedStyle = window.getComputedStyle(this.refDiv);
    const widthInString = computedStyle.getPropertyValue('width');
    const widthInInt = parseInt(widthInString);

    console.log("div.width:", widthInInt);
    console.log("window.getComputedStyle(div):", computedStyle);
    
    return widthInInt;
  }
  getRFromCircle() {
    const computedStyle = window.getComputedStyle(this.refCircle);
    const rInString = computedStyle.getPropertyValue('r');
    const rInInt = parseInt(rInString);

    console.log("circle.r:", rInInt);
    console.log("window.getComputedStyle(div):", computedStyle);
    
    return rInInt;
  }
  render() {
    return (
      <div>
        <div
          ref={(div) => {this.refDiv = div}}
          style={{
            width: 100,
            height: 200,
          }}
        >
          Dummy
        </div>
        <svg xmlns="http://www.w3.org/2000/svg">
          <circle 
            ref={(circle) => {this.refCircle = circle}} 
            cx="100" cy="50" r="30"
          />
        </svg>
      </div>
    );
  }
}

export default App;
App.test.js
import React from 'react';
import { configure, mount } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import App from './App';

configure({ adapter: new Adapter() });

it('div', () => {
  const app = mount(<App />);
  const appInstance = app.instance();
  expect(appInstance.getWidthFromDiv()).toBe(100);
});

it('circle', () => {
  const app = mount(<App />);
  const appInstance = app.instance();
  expect(appInstance.getRFromCircle()).toBe(30);
});

テスト結果

 FAIL  src/App.test.js
  ✓ div (75ms)
  ✕ circle (13ms)

  ● circle

    expect(received).toBe(expected) // Object.is equality

    Expected: 30
    Received: NaN

      15 |   const app = mount(<App />);
      16 |   const appInstance = app.instance();
    > 17 |   expect(appInstance.getRFromCircle()).toBe(30);
         |^
      18 | });
      19 |

      at Object.toBe (src/App.test.js:17:40)

  console.log src/App.js:9
    div.width: 100

  console.log src/App.js:10
    window.getComputedStyle(div): CSSStyleDeclaration {
      '0': 'display',
      '1': 'width',
      '2': 'height',
      _values: { display: 'block', width: '100px', height: '200px' },
      _importants: { display: '', width: '', height: '' },
      _length: 3,
      _onChange: [Function] }

  console.log src/App.js:19
    circle.r: NaN

  console.log src/App.js:20
    window.getComputedStyle(div): CSSStyleDeclaration {
      _values: {},
      _importants: {},
      _length: 0,
      _onChange: [Function] }

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 passed, 2 total
Snapshots:   0 total
Time:        0.749s, estimated 1s
Ran all test suites related to changed files.

Watch Usage: Press w to show more.
Screen Shot 2018-10-15 at 2.06.42.png

ブラウザ上では描画、値の取得ともに成功しているが、テスト時にはCircle要素の属性rの値はNaNとなっている。出力結果には、Circle要素にgetComputedStyle()を実行した時に属性値が取得できていないことが示されているため、Jest特有の事象と考えられる。

Div要素の属性値は取得できている

  console.log src/App.js:10
    window.getComputedStyle(div): CSSStyleDeclaration {
      '0': 'display',
      '1': 'width',
      '2': 'height',
      _values: { display: 'block', width: '100px', height: '200px' },
      _importants: { display: '', width: '', height: '' },
      _length: 3,
      _onChange: [Function] }

Circle要素の属性値は取得できていない

  console.log src/App.js:20
    window.getComputedStyle(div): CSSStyleDeclaration {
      _values: {},
      _importants: {},
      _length: 0,
      _onChange: [Function] }

ワークアラウンド

原因追求はのちに行うとして、現時点で関数getComputedStyle()から期待値を取得するには、関数をモックする必要がある。以下、Sinonを使って実装した。

App.js
import React from 'react';

class App extends React.Component {
  getWidthFromDiv() {
    const computedStyle = window.getComputedStyle(this.refDiv);
    const widthInString = computedStyle.getPropertyValue('width');
    const widthInInt = parseInt(widthInString);
    return widthInInt;
  }
  getRFromCircle() {
    const rInString = this.getR();
    const rInInt = parseInt(rInString);
    return rInInt;
  }
  getR() {
    const computedStyle = window.getComputedStyle(this.refCircle);
    const rInString = computedStyle.getPropertyValue('r');
    return rInString;
  }
  render() {
    return (
      <div>
        <div
          ref={(div) => {this.refDiv = div}}
          style={{
            width: 100,
            height: 200,
          }}
        >
          Dummy
        </div>
        <svg xmlns="http://www.w3.org/2000/svg">
          <circle 
            ref={(circle) => {this.refCircle = circle}} 
            cx="100" cy="50" r="30"
          />
        </svg>
      </div>
    );
  }
}

export default App;
App.test.js
import React from 'react';
import { configure, mount } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import sinon from 'sinon';
import App from './App';

configure({ adapter: new Adapter() });

const stub = sinon.stub(App.prototype, 'getR');
stub.returns("30px");

it('div', () => {
  const app = mount(<App />);
  const appInstance = app.instance();
  expect(appInstance.getWidthFromDiv()).toBe(100);
});

it('circle', () => {
  const app = mount(<App />);
  const appInstance = app.instance();
  expect(appInstance.getRFromCircle()).toBe(30);
});
 PASS  src/App.test.js
  ✓ div (38ms)  ✓ circle (2ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        0.655s, estimated 1s
Ran all test suites related to changed files.

Watch Usage: Press w to show more.

モック対象の外出し

後述の理由によりgetComputedStyle()はモックできないため、その返り値を用いるステートメントも正常な動作を期待できない。このため、以下の2行を別の関数へ外出しする。

App.js(外出し対象)
    const computedStyle = window.getComputedStyle(this.refCircle);
    const rInString = computedStyle.getPropertyValue('r');

外出し前後のソースコード(抜粋)は以下の通り。なお、コンソール出力の行は削除している。

App.js(外出し前)
  getRFromCircle() {
    const computedStyle = window.getComputedStyle(this.refCircle);
    const rInString = computedStyle.getPropertyValue('r');
    const rInInt = parseInt(rInString);
    return rInInt;
  }
App.js(外出し後)
  getRFromCircle() {
    const rInString = this.getR();
    const rInInt = parseInt(rInString);
    return rInInt;
  }
  getR() {
    const computedStyle = window.getComputedStyle(this.refCircle);
    const rInString = computedStyle.getPropertyValue('r');
    return rInString;
  }

新たに作成した関数getR()は本環境において、期待通りに動作すれば"30px"を返すだけの関数である。ここを問答無用で"30px"を返すようモックすると、テストを期待通りにパスする。

App.test.js(モックのみ抜粋)
const stub = sinon.stub(App.prototype, 'getR');
stub.returns("30px");

getComputedStyle()をモックできない理由

getComputedStyle()の戻り値はCSSStyleDeclaration型である。この型は**特定の条件によってのみ得られる読み取り専用の型1**であり、この型の変数を自作することはできない。ゆえにCSSStyleDeclaration型の戻り値は実現できず、getComputedStyle()をモックすることは不可能である。

本来、テストのためのソースコード変更はするべきではないが、テスト出来ない箇所をテストしないという訳にもいかない。そのため、getComputedStyle()を利用する最小限の範囲を別の関数に外出しし、そちらをモックすることでテストを実現した。

  1. https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleDeclaration#Summary

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?