LoginSignup
0
0

More than 1 year has passed since last update.

「テスト書けない」って悩みは、コンポーネント/ファイルの責務を意識すれば解決する気がしたのでReactで実践した

Last updated at Posted at 2022-05-11

これを読んだらどうなる?

テスト書きづらい!書けない!から、テスト書けるかも?とちょっと希望が持てるようになる(と信じてる)

使用技術

  • Node.js
  • React
  • TypeScript
  • Jest
  • React Testing Library

背景

  • テストどうやって書けば良いかわからない、って悩みを持っていた
  • Clean Architectureな構成にすれば、テスト書きやすくなるって聞いた
  • だから フロントエンドでClean Architectureを適用してみる(+サンプルコード) を参考にClean ArchitectureなReactプロジェクトを作って、テスト書いてみた
    • Reactにしたのは、自分が普段使っているから
  • するとびっくり、テストが非常に書きやすい
  • なので、どういったテストを書けば良さそうか、Clean Architectureの各層で実際に書いてみて、自分なりの指針をまとめてみた
    • 最後に今回記事中に使用したコードが載ったリポジトリを共有します

構成

Reactプロジェクトで、全て src 配下にコードを置いています。

src
├── App.css
├── App.tsx
├── db: databaseに関する情報
├── driver: 外部リソースへのアクセス
├── entity: ドメインに関係するモデル群
├── favicon.svg
├── index.css
├── logo.svg
├── main.tsx
├── presenter: viewに必要な情報を生成
│   └── hooks: presenterの役割を果たすためのhooks
├── repository: 外部リソースのデータとentity間のマッピング処理
├── use-case: アプリケーションロジック
├── view: 表示ロジック
│   ├── components: コンポーネント群
│   │   └── features
│   └── routes: ルーティングに関連する情報
└── vite-env.d.ts

テスト

上記の構成のうち、Driver層、Repository層、Entity層、UseCase層、Presenter層、View層に対してテストを書いていきます。
そのうちPresenter層はカスタムフック、View層はReactコンポーネントで実装するため、React Testing Libraryでテストを書いていきます。

Driver層

外部リソースへのアクセスという役割を持っています。テスト対象のコードはこちらです。

todoDriver.ts
const TODO_URL = `${API_URL}/todos`;

export interface ITodoDriver {
  fetchAll: () => Promise<TodoType[]>;
}

export class TodoDriver implements ITodoDriver {
  /**
   * api経由でtodoを全て取得する関数
   */
  public fetchAll = async () => {
    try {
      const response = await axios.get<TodoType[]>(TODO_URL);
      return response.data;
    } catch (error) {
      throw new Error('todo driver failed to fetch todos.');
    }
  };
}

Driver層は、外部リソースとの通信を担当する性質上、axiosをモックするか、テスト用にサーバーを立てるか、の2通りが考えられます。今回は前者のaxiosをモックする方法でテストしていきます。

todoDriver.test.ts
import axios from 'axios';
import {TodoDriver} from './todoDriver';
import {testDbTodos} from '../../__test__/todos/testData';

jest.mock('axios');
const mockedAxios = axios as jest.Mocked<typeof axios>;

const todoDriverInstance = new TodoDriver();

describe('TodoDriver', () => {
  beforeAll(() => {
    jest.clearAllMocks();
  });
  afterEach(() => {
    jest.clearAllMocks();
  });

  describe('fetchAll', () => {
    it('APIアクセスが成功したら、TODOが2つ返却される', async () => {
      const response = {data: testDbTodos};
      mockedAxios.get.mockResolvedValue(response);

      const fetchTodos = await todoDriverInstance.fetchAll();
      expect(fetchTodos.length).toBe(2);
    });

    it('APIアクセスが失敗したら、エラー「todo driver failed to fetch todos.」がthrowされる', async () => {
      mockedAxios.get.mockRejectedValue(() => {
        throw new Error();
      });

      await expect(todoDriverInstance.fetchAll()).rejects.toThrow(
        'todo driver failed to fetch todos.'
      );
    });
  });
});

fetchAll はデータの取得に成功するか、失敗するか、のどちらかなので、2つのテストを書きます。

このようにDriver層では、 ①通信が成功したらデータベースからデータを取得できる②通信が失敗したらエラーが返る 、の二通りのテストを書いていければ良いのかなと思います。

今回は、axiosをモックする、という方法で書いていきましたが、これではaxios内部のバグがあったときに気づくことができません。なのでテスト用のサーバーを立てる、という方法であれば、より堅牢なテストがかけると思います。レスポンスのステータスをテストすることもできそうです。
この方法を実現するために役立つライブラリのひとつが msw です。この記事では紹介しませんが、とても参考になる記事がありましたので、是非見てみて下さいね!

Repository層

外部リソースのデータとEntity間のマッピング処理という役割を持っています。テスト対象のコードはこちらです。

todoRepository.ts
export interface ITodoRepository {
  fetchAll: () => Promise<TodoEntity[]>;
}

export class TodoRepository implements ITodoRepository {
  private readonly todoDriver: ITodoDriver;
  constructor(todoDriver: ITodoDriver) {
    this.todoDriver = todoDriver;
  }

  /**
   * API経由で取得できたTODOをアプリケーション用に整形
   */
  public fetchAll = async () => {
    try {
      const res = await this.todoDriver.fetchAll();
      const data = this.convertForEntity(res);
      return data;
    } catch (error) {
      throw new Error('todo repository failed to fetch todos.');
    }
  };

  private convertForEntity = (todos: TodoType[]): TodoEntity[] => {
    return todos.map((todo) => ({
      ...todo,
      deadlineDate: new Date(),
    }));
  };
}

ここでは、アプリケーション用にデータを整形するロジックを有しています。なので、それがわかるようにテストしていきたいと思います。

todoRepository.ts
import {TodoRepository} from './todoRepository';
import {TodoDriver} from '../../driver/todo/todoDriver';
import {testDbTodos} from '../../__test__/todos/testData';

const todoDriverInstance = new TodoDriver();
const todoRepositoryInstance = new TodoRepository(todoDriverInstance);

describe('TodoRepository', () => {
  beforeAll(() => {
    jest.clearAllMocks();
  });
  afterEach(() => {
    jest.clearAllMocks();
  });

  describe('fetchAll', () => {
    it('todoDriver.fetchAllが成功した場合、取得したデータと同じ数だけdeadlineを追加したデータを返却する', async () => {
      const spyTodoDriver = jest
        .spyOn(todoDriverInstance, 'fetchAll')
        .mockResolvedValue(testDbTodos);

      const todos = await todoRepositoryInstance.fetchAll();

      expect(spyTodoDriver).toHaveBeenCalledTimes(1);
      expect(todos.length).toBe(2);
      for (const todo of todos) {
        expect(todo).toHaveProperty('deadlineDate');
      }
    });

    it('todoDriver.fetchAllが失敗した場合、エラー「todo repository failed to fetch todos.」がthrowされる', async () => {
      const spyTodoDriver = jest
        .spyOn(todoDriverInstance, 'fetchAll')
        .mockRejectedValue(() => {
          throw new Error();
        });

      await expect(todoRepositoryInstance.fetchAll()).rejects.toThrow(
        'todo repository failed to fetch todos.'
      );
      expect(spyTodoDriver).toHaveBeenCalledTimes(1);
    });
  });
});

まず、todoRepository.fetchAll内部で使用している todoDriver.fetchAll をモックします。

todoDriver.fetchAllは成功するか失敗するか、の2パターンの挙動が考えられます。なのでtodoRepository.fetchAllも成功したパターンと失敗したパターンの2つのテストが書ければ良さそうですね。

ここでRepository層の役割をおさらいしましょう。Repository層では「外部リソースのデータとEntity間のマッピング処理」です。つまり、データベースに保存してあるデータをアプリケーション用に変換する、役割を持っています。

なので、 ①データが取得できたら変換後のデータが返ってくる②データの取得に失敗したらエラーを返す というテストケースが考えられそうです。

ちなみに、Repository層を挟むなんて冗長すぎる。。って思う人もいるかと思います。僕も最初はなぜこの層が必要なんだろうと思いましたが、例えばデータベースをfirestore(ドキュメント指向データベース)からMySQL(リレーショナルデータベース)に移行しよう、となったときに効果が絶大と気付きました。
Repository層がないと、データベースの変更によって、アプリケーション内部のロジックを変えないといけません。
けれどもRepository層を挟むことによって、アプリケーションへの影響を最小限に、Repository層の変換ロジックを変更するだけで、移行ができる(はず)のです。(実際に自分がこのようなデータ移行を行ったことがないので、実体験ベースで語れず申し訳ないです)

Entity層

ビジネスロジックを担当する層です。Clean Architecutreにおいて、どこにも依存しない層(あの有名な円の図の一番内側)です。テスト対象のコードはこちらです。

todo.ts
import * as dayjs from 'dayjs';

export type TodoEntity = {
  userId: number;
  id: number;
  title: string;
  completed: boolean;
  deadlineDate: Date;
};

export class Todo {
  private readonly userId: number;
  private readonly id: number;
  private readonly title: string;
  private readonly completed: boolean;
  private readonly deadlineDate: Date;

  constructor(data: TodoEntity) {
    this.userId = data.userId;
    this.id = data.id;
    this.title = data.title;
    this.completed = data.completed;
    this.deadlineDate = data.deadlineDate;
  }

  /**
   * 一位のidを生成する関数
   */
  public generateUniqueId() {
    return `${this.userId}-${this.id}`;
  }

  /**
   * 完了しているかどうかの判定
   */
  public isCompleted() {
    return this.completed;
  }

  /**
   * 締め切りを過ぎているかどうかの判定
   */
  public isDeadlineExceeded = () => {
    const now = dayjs();
    const deadline = dayjs(this.deadlineDate);
    return now.isAfter(deadline, 'day');
  };
}

いくつか、todoに関する関数が存在しますね。ではその関数のテストを書いていきましょう。各関数が、期待値であることをテストします。

todo.test.ts
import {Todo, TodoEntity} from './todo';

const A_DAY_MILLISECOND = 86400000;
const baseTodo: TodoEntity = {
  userId: 1,
  id: 1,
  title: 'dummyTitle',
  completed: false,
  deadlineDate: new Date(),
};
const todoInstance = new Todo(baseTodo);

describe('Todo', () => {
  beforeAll(() => {
    jest.clearAllMocks();
  });
  afterEach(() => {
    jest.clearAllMocks();
  });

  describe('generateUniqueId', () => {
    it('userIdとidを-で繋いだ文字列を返す', () => {
      const uniqueId = todoInstance.generateUniqueId();
      expect(uniqueId).toBe('1-1');
    });
  });

  describe('isCompleted', () => {
    it('completed = trueの場合、trueを返す', () => {
      const todo = {...baseTodo, completed: true};
      const todoInstance = new Todo(todo);
      expect(todoInstance.isCompleted()).toBeTruthy();
    });

    it('completed = falseの場合、falseを返す', () => {
      expect(todoInstance.isCompleted()).toBeFalsy();
    });
  });

  describe('isDeadlineExceeded', () => {
    it('今日が締切日より前の場合、falseを返す', () => {
      const tomorrow = new Date().valueOf() + A_DAY_MILLISECOND;
      const deadlineDate = new Date(tomorrow);

      const todo = {...baseTodo, deadlineDate};
      const todoInstance = new Todo(todo);
      expect(todoInstance.isDeadlineExceeded()).toBeFalsy();
    });

    it('今日が締切日の場合、falseを返す', () => {
      expect(todoInstance.isDeadlineExceeded()).toBeFalsy();
    });

    it('今日が締切日より後の場合、trueを返す', () => {
      const yesterday = new Date().valueOf() - A_DAY_MILLISECOND;
      const deadlineDate = new Date(yesterday);

      const todo = {...baseTodo, deadlineDate};
      const todoInstance = new Todo(todo);
      expect(todoInstance.isDeadlineExceeded()).toBeTruthy();
    });
  });
});

ここでは期待値通りであることをテストしていきました。ひとつ意識した点は、 isDeadlineExceeded のテストで境界値分析を用いて、必要最低限なテストケースを3つと判断したことです。

境界値分析とは、ソフトウェアテストで適切なテストケースを作成する手法の一つで、出力が同じになるような入力をそれぞれグループにまとめ、グループが隣接する境界やその前後の値を入力としてテストを行なう方式。

日付が関わるテストは無限にテストしようと思えばできます。もし本気で全ての日付のテストをするとなったら、いつまで経ってもアプリはリリースできませんね。なので、テストケースの作成は工夫する必要があります。今回は境界値分析という手法を使いましたが、他にもいろんな手法があるので調べてみて下さいね!

さて、これで仮にisAfterの真偽値が逆転したとしてもすぐに気付けるようになりました。

補足ですが、ライブラリを使用している場合は言わずもがなですが、バージョンアップなどを行い最新に保ちたいです。
このとき、テストを書いておくと、バージョンアップして何か破壊的な変更があったとしても、すぐに気づき、修正し、バージョンを最新に保つのに役立ちます。

UseCase層

アプリケーションロジックを担当する層です。アプリケーションの振る舞いをここには記述します。テスト対象のコードはこちらです。

todoUseCase.ts
import {ITodoRepository} from '../../repository/todo/todoRepository';

/**
 * アプリケーションにおける動作を定義
 */
export class TodoUseCase {
  private readonly todoRepository: ITodoRepository;
  constructor(todoRepository: ITodoRepository) {
    this.todoRepository = todoRepository;
  }

  /**
   * 全てのtodoを取得
   */
  public fetchAll = async () => {
    try {
      const todos = await this.todoRepository.fetchAll();
      return todos;
    } catch (error) {
      throw new Error('todo use case failed to fetch todos.');
    }
  };
}

アプリケーションロジックの挙動が期待通りか、テストします。
repositoryのことは考えたくないので、モックします。

todoUseCase.test.ts
import {TodoDriver} from '../../driver/todo/todoDriver';
import {TodoRepository} from '../../repository/todo/todoRepository';
import {testApplicationTodos} from '../../__test__/todos/testData';
import {TodoUseCase} from './todoUseCase';

const todoDriverInstance = new TodoDriver();
const todoRepositoryInstance = new TodoRepository(todoDriverInstance);
const todoUseCaseInstance = new TodoUseCase(todoRepositoryInstance);

describe('TodoUseCase', () => {
  beforeAll(() => {
    jest.clearAllMocks();
  });
  afterEach(() => {
    jest.clearAllMocks();
  });

  describe('fetchAll', () => {
    it('todoRepository.fetchAllが成功した場合、TODOが2つ返却される', async () => {
      const spyTodoRepository = jest
        .spyOn(todoRepositoryInstance, 'fetchAll')
        .mockResolvedValue(testApplicationTodos);

      const todos = await todoUseCaseInstance.fetchAll();

      expect(spyTodoRepository).toHaveBeenCalledTimes(1);
      expect(todos.length).toBe(2);
    });

    it('todoRepository.fetchAllが失敗した場合、エラー「todo repository failed to fetch todos.」がthrowされる', async () => {
      const spyTodoRepository = jest
        .spyOn(todoRepositoryInstance, 'fetchAll')
        .mockRejectedValue(() => {
          throw new Error();
        });

      await expect(todoUseCaseInstance.fetchAll()).rejects.toThrow(
        'todo use case failed to fetch todos.'
      );
      expect(spyTodoRepository).toHaveBeenCalledTimes(1);
    });
  });
});

データ取得に関連するので、これも似たようなテストですね。

他に関数を定義したとしても、その関数の役割は何か、を明確にすれば自ずとテストが書きやすくなってくると思います。

少し思ったのは、アプリケーションがスケールしてくると、この層のテストは少し迷いそうな印象を受けています。例えば、今はTODOのデータを取得するだけですが、関連する情報(例えばユーザー情報)を同時に取得する必要があるかもしれませんし、他のusecase層で定義したアプリケーションロジックを呼びだすことも考えられます。

でもそうなったら、呼びだしたアプリケーションロジックはモックして、呼び出したことだけテストしてあげれば良いかなと思っています(そのロジックに対しても、ユニットテストを書く前提ではありますが)。

Presenter層

viewに必要な情報を生成を担当する層です。Reactを使用しているので、ここはカスタムフックで実装します。テスト対象のコードはこちらです。

useFetchTodos.ts
import {useCallback, useEffect, useState} from 'react';
import {TodoEntity} from '../../../entity/todo/todo';
import {TodoUseCase} from '../../../use-case/todo/todoUseCase';

export const useFetchTodos = (todoUseCaseInstance: TodoUseCase) => {
  const [todos, setTodos] = useState<TodoEntity[]>([]);

  const fetchTodos = useCallback(async () => {
    return await todoUseCaseInstance
      .fetchAll()
      .then((res) => setTodos(res))
      .catch((error) => console.log(error));
  }, [todoUseCaseInstance]);

  useEffect(() => {
    const unsub = fetchTodos();
    return () => {
      unsub;
    };
  }, [fetchTodos]);

  return {todos};
};

TODOを取得する役割を持ったカスタムフックです。説明を簡潔にするため、TODOの取得に失敗したら、consoleに出力するだけにします。
このようにカスタムフックの役割が明確であると、テストが書きやすそうですね。ではテストを書いていきます。

useFetchTodos.test.ts
/**
 * @jest-environment jsdom
 */

import {testApplicationTodos} from '../../../__test__/todos/testData';
import {renderHook, waitFor} from '@testing-library/react';
import {useFetchTodos} from './useFetchTodos';
import {TodoDriver} from '../../../driver/todo/todoDriver';
import {TodoRepository} from '../../../repository/todo/todoRepository';
import {TodoUseCase} from '../../../use-case/todo/todoUseCase';

const todoDriverInstance = new TodoDriver();
const todoRepositoryInstance = new TodoRepository(todoDriverInstance);
const todoUseCaseInstance = new TodoUseCase(todoRepositoryInstance);

describe('useFetchTodos', () => {
  beforeAll(() => {
    jest.clearAllMocks();
  });
  afterEach(() => {
    jest.clearAllMocks();
  });

  it('todoの取得に成功したら、todoが2個取得できる', async () => {
    const spyTodoUseCaseInstance = jest
      .spyOn(todoUseCaseInstance, 'fetchAll')
      .mockResolvedValue(testApplicationTodos);

    const {result} = renderHook(() => useFetchTodos(todoUseCaseInstance));
    await waitFor(() => expect(spyTodoUseCaseInstance).toHaveBeenCalled());
    await waitFor(() => expect(result.current.todos.length).toBe(2));
  });

  it('todoの取得に失敗したら、todoが0個取得できる', async () => {
    const spyTodoUseCaseInstance = jest
      .spyOn(todoUseCaseInstance, 'fetchAll')
      .mockRejectedValue(() => {
        throw new Error();
      });
    const {result} = renderHook(() => useFetchTodos(todoUseCaseInstance));
    await waitFor(() => expect(spyTodoUseCaseInstance).toHaveBeenCalled());
    await waitFor(() => expect(result.current.todos.length).toBe(0));
  });
});

カスタムフックなので、まずテストファイル上部に

/**
 * @jest-environment jsdom
 */

という宣言が必要です。

今回は、TODOを取得するカスタムフックなので

  • ①TODOの取得に成功したら2個のTODOが返ること
  • ②TODOの取得に失敗したら0個のTODOが返ること

をテストすれば良さそうです。仮にTODOの取得に失敗したときに失敗フラグを返すようになったとしても、失敗したら0個のTODOと失敗フラグを返す、みたいなテストに変えれば良さそうですね。

また、カスタムフック内で作成した関数を返したい時があると思います。
その関数のテストをするときは、 act で囲んで関数を実行してから、

  • ①関数の返り値が期待値通りか
  • ②呼び出されて欲しいuseCaseが呼び出されているか

などをテストしてあげれば良いかなと思います。
参考になるドキュメントを見つけたので、以下にURLを添付しておきます。

View層

表示ロジックを担当する層です。Reactコンポーネントで実装します。この層は、コンポーネントの役割を意識したコーディングをしなければテストが難しくなると考えています。
テスト対象のコードはこちらです。

TodoList.tsx
import React from 'react';
import {TodoEntity} from '../../../../entity/todo/todo';
import {Todo} from './Todo';

interface P {
  todos: TodoEntity[];
}

export const TodoList = ({todos}: P) => {
  return (
    <div>
      {todos.map((todo, index) => (
        <Todo key={`${todo.userId}-${todo.id}-${index}`} todo={todo} />
      ))}
    </div>
  );
};

Todo.tsx
import React from 'react';
import {TodoEntity} from '../../../../entity/todo/todo';

interface P {
  todo: TodoEntity;
}

export const Todo = ({todo}: P) => {
  return <div data-testid="todo-component">{todo.title}</div>;
};

TodoList.tsx は渡されたtodoの数だけ Todo.tsx を表示する役割、 Todo.tsx は渡されたtodoのtitleを表示する役割、という風に捉えられるかと思います。では実際にテストを書いていきましょう。

TodoList.test.tsx
/**
 * @jest-environment jsdom
 */

import React from 'react';
import {render, screen} from '@testing-library/react';
import {testApplicationTodos} from '../../../../__test__/todos/testData';
import {TodoList} from './TodoList';

describe('TodoList', () => {
  it('propsで渡された数だけ、Todoを描画する', () => {
    render(<TodoList todos={testApplicationTodos} />);
    expect(screen.getAllByTestId('todo-component').length).toBe(
      testApplicationTodos.length
    );
  });
});

Todo.test.tsx
/**
 * @jest-environment jsdom
 */

import React from 'react';
import {render, screen} from '@testing-library/react';
import {applicationTodo} from '../../../../__test__/todos/testData';
import {Todo} from './Todo';
import {TodoEntity} from '../../../../entity/todo/todo';
import '@testing-library/jest-dom';

describe('Todo', () => {
  it('propsで渡されたtodoのtitleを表示する', () => {
    const todo: TodoEntity = {...applicationTodo, title: 'test title'};
    render(<Todo todo={todo} />);

    expect(screen.getByTestId('todo-component')).toHaveTextContent(
      'test title'
    );
  });
});

TodoList.tsx は渡されたtodoの数だけ Todo.tsx を表示する役割、 Todo.tsx は渡されたtodoのtitleを表示する役割

というようにコンポーネントの役割を定義したので、それに沿ったテストを行いました。

この粒度ならまだまとめても良いかもしれませんが、意識しないとすぐにコンポーネントが大きくなります。
コンポーネントの役割を明確にしなければ、すぐにテストしづらいコンポーネントになってしまいます。
ファイルが増えて面倒だな、、と思うかもしれません。
しかし、今綺麗にすることをサボったら、将来きっともっと面倒なことが起きるはずです。気をつけていきたいですね。


※ 5/13 追記

DOM要素の特定に、 getByTestId を使用していました。ただし、React Testing Libraryの公式ドキュメントを見てみると、

getByTestId: The user cannot see (or hear) these, so this is only recommended for cases where you can't match by role or text or it doesn't make sense (e.g. the text is dynamic).
参照: https://testing-library.com/docs/queries/about/#priority

とあるように、DOM要素の特定にどうにもならない時だけ使用するように、と記述がありました。

In this way, the library helps ensure your tests give you confidence that your application will work when a real user uses it.
参照: https://testing-library.com/docs/#the-solution

とあり、React Testing Library は実際のユーザーがアプリケーションを使用したときにアプリケーションが動作する、ということをテストできるようにするのが好まれる使用方法だと考えられます。 getByTestId というDOM要素の特定は、実際のユーザーがアプリケーションを使用したときの動作とはかけ離れたところにあるため、どうにもならない時だけ使用する必要がある、と考えられます。

ではどういったDOM要素を特定するAPIを使用すればいいのかというと、以下のページに記載あるとおり、基本的には

  • getByRole
  • getByLabelText
  • getByPlaceholderText
  • getByText
  • getByDisplayValue

を使用しましょう。どういったときにそれぞれ使用すればいいかは、後述のページをご参照いただければ幸いです。


まとめ

  • 責務を意識して分離しましょう
  • コンポーネント/ファイルの役割を定義してみましょう
  • そしたらどういったテストをすれば良いか、明確になる気がする
    • どういったロジックのテストなのか
    • どういった見た目のテストなのか
  • ただし、これがスケールしてきた時、同じようなこと言っていられるかはわからない。。

終わりに

Clean Architectureの構成で作ったReactプロジェクトで各層に対してテストを書いていきました。テストを書くのが苦じゃないと思ったのは、自分にとってすごい発見です。それだけ今までテスト書きづらいコードを書いてきたってことですね。。正直書きやすいと、テスト書くのが楽しいです。
これを機に、もっとテストのことに詳しくなりたいなと思いました。

書いていく中で、React Testing Libraryを使ったReduxのテストとかどう書くんだろう、、って思ったので、次はそこを深ぼっていきたいと思います。

長くなってしまいましたが、これで以上です。最後まで読んでいただきありがとうございました!

今回のリポジトリ

参考資料

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