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?

仕事の効率化 → まず出社文化をなくせ

社内でしか出来ない作業があるなら致し方ないが、
熱中症と脱水症状のリスクが伴うこのクソ暑い中、
「元気に会社に出勤しろよ!」みたいな会社、ホントになくなればいいのにね!

暑い中出社した時点で、既にHP3ぐらいまで擦り減って思考も停止してる状態で、
効率よく仕事なんか出来るわけねぇって早く理解してほしい。。。

デバイスごとにテストをする雛形

愚痴ったところで社会が変わるわけでもなく、この歳で転職もリスクしか無いので愚痴ぐらいはここで吐き出してスッキリしたところで、今回はコード量多めの雛形を作っていくよ。
※めんどくさいので、色々とベタで書いてる部分はあるからいい感じにして使ってくれよな!

定義やら共通・汎用的な関数やら
// モードの設定
type ModeType = 'light' | 'dark';
const modeList: Array<ModeType> = ['light', 'dark'];

// テストデバイス定義
export interface Device {
  device: string;
  os: string;
  mode?: ModeType;
}
// 表示モードについては後で設定するので、
// ここではテストしたいデバイス情報のみ設定した
const testDevice: Array<Device> = [
  {
    device: 'iPhone 13',
    os: 'iOS',
  },
  {
    device: 'Galaxy S5',
    os: 'Android',
  },
];

// テスト用パラメータ
export interface TestParam {
  device: Device;
  param?: any; // テストで使いたい値や事前ログインさせるなどの情報を設定すればいいよ
}

// テストサイト用URL
const baseUrl = 'http://localhost:8100/';

let browser: puppeteer.Browser;

// jasmineを使ってるので共通の前処理とかの定義
export function setupBrowserHooks(): void {
  jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000000;
  beforeAll(async () => {
    // ブラウザの起動
    browser = await puppeteer.launch({
      headless: false,
    });
  });
  afterEach(async () => {
    if (browser) {
      const pages = await browser.pages();
      if (pages.length > 1) {
        for (let i = pages.length - 1; i > 0; i--) {
          await pages[i].close();
        }
      }
    }
  });

  afterAll(async () => await browser?.close());
}

// タブの生成
export async function getBrowserState(device: Device): Promise<{
  browser: puppeteer.Browser;
  page: puppeteer.Page;
}> {
  if (!browser) {
    throw new Error(
      'setupBrowserHooks()を先に呼べ!',
    );
  }
  const page = await browser.newPage();
  await setEmulate(page, device);
  return {
    browser,
    page,
  };
}

// ブラウザのエミュレート設定
export async function setEmulate(
  targetPage: puppeteer.Page,
  device: Device,
): Promise<puppeteer.Page> {
  // ライト・ダークモードの切替
  await targetPage.emulateMediaFeatures([
    {
      name: 'prefers-color-scheme',
      value: device.mode,
    },
  ]);
  // 見た目(デバイス)の設定
  await targetPage.emulate(puppeteer.KnownDevices[device.device]);
  return targetPage;
}

// page.waitFor~でも処理は待てそうだけど、時間指定で待ちたいとか用にスリープ関数
export const sleep = (milliseconds) =>
  new Promise((resolve) => setTimeout(resolve, milliseconds));

// ページ再読み込み
export const resetPage = async (page) => {
  await page.goto(`${baseUrl}`, {
    waitUntil: ['domcontentloaded'],
    timeout: 3000,
  });
  await sleep(3000);
};

// スクショ
export const screenshot = async (screenshotParam: {
  page: puppeteer.Page;
  path: string;
  fileName: string;
  newPage?: puppeteer.Page; // テストページから別タブで開いた情報などをスクショしたい時用
}) => {
  // 出力先はとりあえずこのファイルが格納されてるフォルダの
  // 兄弟フォルダにscreenshotsというフォルダを作ってそこに入れる
  const outPath = `${__dirname}/../screenshots/${screenshotParam.path}`;
  if (!existsSync(outPath)) {
    mkdir(outPath, { recursive: true }, (err) => {
      if (err) {
        console.debug(`mkdirErr:${err}`);
      }
    });
  }
  if (screenshotParam.newPage) {
    await screenshotParam.newPage.screenshot({
      path: `${outPath}/${screenshotParam.fileName}.png`,
      fullPage: true,
      omitBackground: true,
    });
  } else {
    await screenshotParam.page.screenshot({
      path: `${outPath}/${screenshotParam.fileName}.png`,
      fullPage: true,
      omitBackground: true,
    });
  }
};

// イベントを起こして待機させた後にスクショを撮る
export const actionsSleepScreenshot = async (
  actions: Array<{
    action: () => Promise<void>;
    sleepTime: number;
    subFileName: string;
  }>,
  screenshotParam: {
    page: puppeteer.Page;
    path: string;
    newPage?;
  },
) => {
  let snapIndex = 0;
  await screenshot({
    ...screenshotParam,
    fileName: `${snapIndex}_初期表示`,
  });
  for (const action of actions) {
    snapIndex++;
    await action.action();
    await sleep(action.sleepTime);
    await screenshot({
      ...screenshotParam,
      fileName: `${snapIndex}_${action.subFileName}`,
    });
  }
};


// ITを生成する関数
export function createIt(param: {
  title: string; // シナリオ名
  testParam: TestParam;  // テスト用パラメータ
  initAction?: (
    browser: puppeteer.Browser,
    page: puppeteer.Page,
  ) => Promise<void>; // テスト実施前の事前処理
  action: (browser: puppeteer.Browser, page: puppeteer.Page) => Promise<void>; // シナリオ処理
}) {
  // テスト生成
  it(param.title, async () => {
    try {
      // ブラウザ情報と生成されたタブ情報を取得
      const { browser, page } = await getBrowserState(param.testParam.device);
      
      // とりあえずページ読み込み
      await resetPage(page);

      // 事前ログインとか共通的になんか入れ込んでおきたい時とかの場合に、
      // こことかでゴリゴリに実装すればいいよ
      // if (param.testParam.~~~) {
      // }
      
      // テスト前処理
      if (param.initAction) {
        await param.initAction(browser, page);
      }
      
      // 実際のテスト処理
      await param.action(browser, page);
    } catch (e) {
      console.error(e);
      throw e;
    } finally {
      // ローカルストレージのリセットとか何かアレばぶち込む
      const pages = await browser.pages();
      pages
        .filter((page) => page.url().startsWith('http://localhost:8100'))
        .forEach(async (page, index) => {
          await page.evaluate(() => {
            const strageKeys = [
              'CapacitorStorage.jwt', // JWT
              'CapacitorStorage.test', // テスト
            ];
            strageKeys.forEach((key) => localStorage.removeItem(key));
          });
        });
    }
  });
}

上記がアレば、あとはゴリゴリとシナリオを考えて、ルールに沿って実装していけば素敵やん

メイン部分
const sleepTime = 2000;
describe('E2E テスト', () => {
  // デバイスごとにライトモード・ダークモードで同じ操作を行う想定で作ってる
  for (const device of testDevice) {
    for (const mode of modeList) {
      describe(`${device.os} ${mode}`, () => {
        const testParam: TestParam = {
          device: { ...device, mode },
          param: undefined,
        };

        describe(`画面(URL)ごととか機能ごとのテストを書いていく`, () => {
          setupBrowserHooks(); // ブラウザの起動とか

          // XXX 初期表示確認
          createIt({,
            title: `初期表示`,
            testParam,
            action: async (browser, page) => {
              // スクショだけとって終わりみたいなケース
              await screenshot({
                page,
                path: `〇〇機能/初期表示/${device.os}/${device.mode}`,
                fileName: `初期表示`,
              });
              // TODO 画面の項目を何かしら確認がしたい場合はここをちゃんと書く
              expect(true).toBeTruthy();
            },
          });

          // XXX 複合処理
          createIt({,
            title: `初期表示`,
            testParam,
            action: async (browser, page) => {
              // 複数のイベントをまとめて実行
              await actionsSleepScreenshot(
                [
                  {
                    action: () =>
                      page.click(
                        `ion-footer ion-segment-button[value="favorite"]`,
                      ),
                    sleepTime,
                    subFileName: `お気に入りタブ選択`,
                  },
                  {
                    action: () =>
                      page.click(
                        `ion-footer ion-segment-button[value="all"]`,
                      ),
                    sleepTime,
                    subFileName: `すべてタブ選択`,
                  },
                ],
                {
                  page,
                  path: `〇〇画面/フッタータブ切替/${device.os}/${device.mode}`,
                },
              );
              // TODO 画面の項目を何かしら確認がしたい場合はここをちゃんと書く
              expect(true).toBeTruthy();
            },
          });
          
        });
      });
    }
  }
});

createIt部分を別ファイルとかで定義すれば、機能ごとにテストシナリオをまとめることも出来るし、
コメントアウトとかもしやすくなりテスト実施の効率化とかになるのかなと。
あとは一通りのケースが書ければ、デグレテスト(なんだっけ、リグレッションテストだっけ?)で使えるので、
初回リリースとかで時間が全くない状態のときはこういったテストコードが作れないのは仕方がないにしろ、
その後の運用・保守工程とかでこういったコードをちゃんと残せるようになれば、今後の改修とか何とかも色々と楽になるのかなということで、どんどん作っていけばいいのになぁ。

こういうモノこそ、新人とかにやらせれば元の機能に影響なく、プログラムをいじって色々と出来るようになるから、OJTとかにもってこいの内容なんだけどなぁ。
Node・TypeScript使うし、システムの理解とかも動かしながら作るわけだから自然と身についていくだろうし、画面を弄るんだからどういうことを気をつけなきゃいけないとか気づけるだろうし、テストケースが量産されて書き方とかもどんどん理解が深まれば他のプロジェクトとかでも流用できるだろうしでいい事づくしなんだけどなぁ。。。

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?