概要
Jestでテストを行った際にSVG要素の属性値を取得できなかったため、モックで対処した件の覚書。
ソースコード
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;
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.
ブラウザ上では描画、値の取得ともに成功しているが、テスト時には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を使って実装した。
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;
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行を別の関数へ外出しする。
const computedStyle = window.getComputedStyle(this.refCircle);
const rInString = computedStyle.getPropertyValue('r');
外出し前後のソースコード(抜粋)は以下の通り。なお、コンソール出力の行は削除している。
getRFromCircle() {
const computedStyle = window.getComputedStyle(this.refCircle);
const rInString = computedStyle.getPropertyValue('r');
const rInInt = parseInt(rInString);
return rInInt;
}
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"
を返すようモックすると、テストを期待通りにパスする。
const stub = sinon.stub(App.prototype, 'getR');
stub.returns("30px");
getComputedStyle()
をモックできない理由
getComputedStyle()
の戻り値はCSSStyleDeclaration型である。この型は**特定の条件によってのみ得られる読み取り専用の型1**であり、この型の変数を自作することはできない。ゆえにCSSStyleDeclaration型の戻り値は実現できず、getComputedStyle()
をモックすることは不可能である。
本来、テストのためのソースコード変更はするべきではないが、テスト出来ない箇所をテストしないという訳にもいかない。そのため、getComputedStyle()
を利用する最小限の範囲を別の関数に外出しし、そちらをモックすることでテストを実現した。