【Javascript】JestでFileオブジェクトのテストをする方法


はじめに

Jestを使ったユニットテストで、Fileオブジェクトを利用したメソッドのテストを実施したい。
FileオブジェクトはHTMLの<input type="file">要素から得られる。

しかし、Jestのユニットテストでは、HTMLを介さずローカルのファイルからFileオブジェクトを生成したい。

ローカルファイルからFileオブジェクトを生成するにあたってBufferオブジェクトを扱う必要があるのですが、小さいサイズのBufferは共有メモリで扱われるということを知らず詰まってしまったしまいました。

この記事ではFileオブジェクトのテスト方法と注意すべき点を解説します。

※最終的なローカルファイルからFileオブジェクト生成のコードはこちらに記載しています

テスト方法

以下にJestでのFileオブジェクトのテストの手順を示す。

テスト対象のメソッド

テスト対象のメソッドは以下のコードfiles.tsである。
処理内容としては、引数にテキストファイルのFileオブジェクト受け取り、改行で区切りnumberのArrayにするというものである。

files.ts
export function readCsvToArray(file: File, maxRow: number): Promise<number[]> {
  return new Promise((resolve, reject) => {
    const fr = new FileReader();
    fr.onload = function(event) {
      const res = (event.target?.result as string)
        .split('\n')
        .map(value => parseInt(value, 10))
        .filter(v => !!v);
      if (res.length > maxRow) {
        reject(`アップロードできるファイルは${maxRow}行以下です`);
      }
      resolve(res);
    };
    fr.onerror = function() {
      reject('ファイルの読み込みに失敗しました');
    };

    fr.readAsText(file);
  });
}

このメソッドは本来であれば、HTMLの<input>要素から受け取ったFileオブジェクトを引数とする。
しかしJestではローカルのファイルからFileオブジェクトを生成したい。以下に手順を示す。

ローカルファイルからのFileオブジェクト生成

Fileコンストラクタ

まず、Fileオブジェクトを生成するためにコンストラクタのドキュメントを見てみる。
コンストラクタの第一引数にはDOMString, Blob, ArrayBufferのいずれか、もしくはこれらのArrayを指定する必要がある。

それぞれの型について調べてみる。
DOMStringはDOMで記述された文字列。
BlobはFileの継承元で、コンストラクタにはArrayBuffer、Blob、USVStringのArrayが必要。USVStringはUnicodeの文字列。

ArrayBufferはバイナリデータのバッファを示すために使用するデータタイプ。Bufferオブジェクトのbufferプロパティを呼び出すと得られる。
Bufferオブジェクトを取得する方法としてFileStreamのcreateFileSyncメソッドがある。createFileSyncメソッドは第一引数にファイルパスを受け取り、Bufferオブジェクトを返す。つまり、次の手順でローカルファイルからFileオブジェクトを生成できそうである。

ローカルファイルからFileオブジェクトの生成手順

  1. FileStreamのcreateFileSyncメソッドにファイルパス(文字列)を渡し、Bufferオブジェクトを取得
  2. Bufferオブジェクトのbufferプロパティを呼び出し、ArrayBufferオブジェクトを取得
  3. ArrayBufferをFileコンストラクタに渡し、Fileオブジェクトを取得

テストコード作成

ローカルファイルからFileオブジェクトの生成手順に沿って以下のテストコードfiles.spec.tsを作成。

テストコード内のgetUploadedCsvBufferメソッドを呼び、ArrayBufferを取得。これをFileコンストラクタの第一引数に渡す。また、第2引数にはファイル名、第3引数はファイル属性のオプションを渡している。

テストデータtest.csvも作成。ファイルサイズは6バイトである。(このファイルサイズが後に問題が起こす...)

テスト対象のコードfiles.ts、テストコードfiles.spec.ts、テストデータtest.csvをすべて同じディレクトリに保存する。

files.spec.ts
import { readCsvToArray } from './file';
import fs from 'fs';

const getUploadedCsvBuffer = (): ArrayBuffer => {
  const path = `${__dirname}/test.csv`; // テスト対象のコード
  const buffer = fs.readFileSync(path);  // Buffer
  return buffer.buffer // ArrayBuffer
};

describe('readCsvToArray', () => {
  // 正常系
  it('return number of array', async () => {
    const file = new File([getUploadedCsvBuffer()], 'test.csv', {
      type: 'text/csv',
    });
    const result = await readCsvToArray(file, 100);
    expect(result).toEqual([1,2,3]);
  });
  // エラー
  it('throw error of over max row', async () => { 
    const file = new File([getUploadedCsvBuffer()], 'test.csv', {
      type: 'text/csv',
    });
    try {
      await readCsvToArray(file, 1);
    } catch (e) {
      expect(e).toEqual('アップロードできるファイルは1行以下です');
    }
  });
});

test.csv
1
2
3

いざテスト

テストの準備が出来たので実行。

実行結果

なぜか、テストデータに記載していないはずの0が含まれている。

原因調査

記載していないデータが含まれているので、バッファ周りが怪しいと思い、Bufferオブジェクトについて調査してみた。
するとbuf.bufferbuf.bufferで得られるArrayBufferは、必ずしも元のBufferとは対応しない。詳しくはbuf.byteOffsetに記述。とあった。

そこでbuf.byteOffsetをみると、Bufferのpoolsizeより小さいバッファはスライスとオフセットを使用しないと別のバッファからのデータを含むとある。

小さいサイズのバッファは共有メモリとして扱われてしまうため、buf.bufferでアクセスしたときに意図しないデータが含まれてしまうということである。

Buffer.poolsizeはデフォルトで8192バイトである。今回のテストデータはこのpoolsizeを下回るため、意図しないデータが含まれてしまった。今回のテストで生成されたブッファのサイズを見てみると8192バイトであり、テストデータの12バイトとは異なる。

バッファから任意のデータと取得するためには、bufferオブジェクトのsliceメソッドに、取得したブッファのオフセットと長さを指定することで取得が可能である。

テストコード変更

ArrayBufferを返すメソッドgetUploadedCsvBufferの返り値をスライスするように変更。

files.spec.ts
const getUploadedCsvBuffer = (): ArrayBuffer => {
  const path = `${__dirname}/test.csv`; // テスト対象のコード
  const buffer = fs.readFileSync(path);  // Buffer
- return buffer.buffer // ArrayBuffer
+ return buffer.buffer.slice(
+   buffer.byteOffset,
+   buffer.byteOffset + buffer.byteLength
+ ); 

};

実行結果

無事にテストを実行することができた。

おわりに

ローカルファイルからFileオブジェクトの生成手順は以下のような流れである。
1. FileStreamのcreateFileSyncメソッドにファイルパス(文字列)を渡し、Bufferオブジェクトを取得
2. Bufferオブジェクトのbufferプロパティを呼び出し、ArrayBufferオブジェクトを取得
3. ArrayBufferをFileコンストラクタに渡し、Fileオブジェクトを取得

BufferオブジェクトのbufferプロパティでArrayBufferを取得する際には注意が必要である。バッファサイズが8192Byte以下のときは、バッファは共有メモリで扱われるため明示的に位置を指定する必要がある。

ローカルファイルからFileオブジェクト生成のコードを示します。

files.spec.ts
const getArrayBuffer = (): ArrayBuffer => {
  const path = `${__dirname}/test.csv`; // テスト対象のコード
  const buffer = fs.readFileSync(path);  // Buffer
  return buffer.buffer.slice(
  buffer.byteOffset,
  buffer.byteOffset + buffer.byteLength
  );  
};

const file = new File([getArrayBuffer()], 'test.csv', {
  type: 'text/csv',
});