Dartのテストカバレッジを正確に計測するDartCLIを公開しました。
以前DartのPackage周りについてという記事で予告していたので、今回はDartのCLIツールを開発してpub.devに公開していくまでを記事にしようと思います。
この記事を読むことでわかること
- DartCLIツールの開発方法
- Dartパッケージ・DartCLIツールのpub.devへの公開方法
今回開発するCLIツール
そもそもdartのCLIツールとはターミナルから利用できるソフトウェアで有名なところだとfvmやdart_frogなどがあります。pub.devに公開されているものについては下記のコマンドを叩くことで、自分のPCでどこからでも利用できるようになります。
dart pub global activate <パッケージ名>
そして、今回開発したいのはDartのテスト周りの便利ツールです。
最終的にはDartのテスト周りを色々と便利にするツールにしたいですが、まず最初の機能としてはテストカバレッジの対象をプロジェクト全体にする機能を用意したいと考えています。どういうことかというとこちらのスクラップをご覧いただくと課題と解決策がわかると思います。スクラップではシェルスクリプトを実行することで対応しているのですが、利用したいプロジェクトに毎回シェルスクリプトを用意しないといけないことを考えるとDartのCLIツールとして開発した方が便利そうなので、今回開発することにしました。
dart createから実装まで
今回開発するにあたって最初のdart createから実装までは下記の4つに分けられます。
- ①dart createでプロジェクトを作成
- ②CommandRunnerの実装
- ③ローカル環境でのGlobal Activate
- ④ビジネスロジックの実装
①は簡単なのでさらっと読んでください。
②③はDartCLIのベースとなる実装とデバッグ方法なので、DartCLIに興味がある人は読んでください。ライブラリパッケージの公開だけに興味がある方は読み飛ばしても大丈夫です。
④については今回開発したCLIの実装についてなので、興味のある方だけ読んでもらえれば大丈夫です!
①dart createでプロジェクトを作成
まずはプロジェクトを作成します。今回はpub.devに公開したいので、パッケージのテンプレートを利用します。
$ dart create -t package dart_test_tools
普段はfvmを利用してFlutterとDartのversionを管理しているのですが、今回はDartプロジェクトなのでdvmxを利用してみます。
$ dart pub global activate dvmx
dvmxはfvmにインスピレーションを受けておかやまんさんが開発されているパッケージでfvmライクにDartのversionを管理できるツールです。dvmxもDartCLIツールなのでdart pub global activateで利用できるようになります。
特にこだわりはないので、最新のDart versionを利用することにします。
$ dvm use -l
config.jsonが生成され最新の3.3.1が設定されました。
{
"dartSdkVersion": "3.3.1"
}
dvmの準備が整ったところで、実装に移っていきます。
②CommandRunnerの実装
基本的に実際の処理などはlib/src/配下に書いていきます。そして、lib/dart_test_tools.dartにpublicに公開するファイルを定義して、bin/main.dartから呼び出して使う感じです。
fvmのリポジトリを参考にしていきます。
まず、fvmのリポジトリのbin/main.dartを見てみるとCommandRunnerクラスを使って色々なコマンドを呼び出していることがわかりました。これを真似してlib/src/runner.dartを作成し、DartTestToolsCommandRunnerクラスを作成します。
こちらはargsパッケージのCommandRunnerクラスを継承しているようなので、pub add していきます。
dvm dart pub add args
CommandRunnerの仕組みはざっくりいうと作成したいコマンドごとにCommandクラスを定義して、そのコマンドをCommandRunnerに追加するだけでオプションを使って簡単にコマンドを分岐させれるようになっているみたいです。今回はCoverageを全てのファイルで計測対象にしたいのでFullCoverageコマンドを作成します。
まず/lib/src/commands/full_coverage.dartを用意します。
import 'package:args/command_runner.dart';
class FullCoverage extends Command<int> {
String get name => 'full_coverage';
String get description => 'A command that has full coverage';
Future<int> run() async {
print('This is a command that has full coverage');
return 0;
}
}
これはGitHub Copilotが出してくれたコードそのままですが、一旦問題なく動きました。
nameはこのコマンドを呼び出す時の名前で、descriptionはコマンドが見つからない時のコマンド一覧で表示されるようです。dartのコマンドを叩くときにtypoするとよくみるあれです。
次に/lib/src/command_runner.dartを用意します。
import 'package:args/command_runner.dart';
import 'package:dart_test_tools/src/commands/full_coverage.dart';
const executableName = 'dart_test_tools';
const executableDescription = 'Dart test tools';
class DartTestToolsCommandRunner extends CommandRunner<int> {
DartTestToolsCommandRunner() : super(executableName, executableDescription) {
addCommand(FullCoverage());
}
}
CommandRunnerクラスを継承し、コンストラクタの処理でFullCoverageを追加しています。
最後に/bin/dart_test_tools.dartを用意します。
import 'package:dart_test_tools/src/command_runner.dart';
void main() {
DartTestToolsCommandRunner().run(["full_coverage"]);
}
一旦はfull_coverageをそのままハードコードしています。
ここまできたらdart runコマンドを実行します。
$ dvm dart run
Building package executable...
Built dart_test_tools:dart_test_tools.
This is a command that has full coverage
これでFullCoverageクラスのrunメソッドが呼ばれました!
CLIで引数を受け取る場合
void main(List<String> args) {
DartTestToolsCommandRunner().run(args);
}
③ローカル環境でのGlobal Activate
次にこれをdart pub global activateで呼べるようにします。普段はpub.devに上がっているものを利用できるようにする時に使うコマンドですが、オプションをつけることでローカルにあるDartのCLIツールを呼べるようにできます。
その前に下準備としてpubspec.yamlに下記を追加します。
executables:
dart_test_tools: dart_test_tools.dart
こうすることでdart pub global activateで実行可能になります。
$ dvm dart pub global activate -s path .
プロジェクトのルートディレクトリにいる場合は上記のようにカレントディレクトリを指定するだけで良いです。(他の場所にいる場合は適切にパスを指定してあげましょう。)
実際に呼んでみましょう。
$ dart_test_tools
Building package executable...
Built dart_test_tools:dart_test_tools.
This is a command that has full coverage
実行できました!!
また、2回目以降はbuildがなくなるようです。
$ dart_test_tools
This is a command that has full coverage
④ビジネスロジックの実装
今回実装する内容は「プロジェクト内のlibは以下のファイルを全てimportするtest/coverage_test.dartを生成する。」です。
これを分解して考えると下の4つの処理で実現できそうです。
1 現在のプロジェクトがDartProjectであるかを確認する。
2 現在のプロジェクトのlib配下のファイルのパスを全て取得する。
3 元々用意してあるcoverage_test.dartの中身に2のファイルパスを埋め込む。
4 test/coverage_test.dartを生成する。
それでは実装に移ります。
1 現在のプロジェクトがDartProjectであるかを確認する。
今回OSのファイルシステムにアクセスして、カレントディレクトリやディレクトリ構造などを扱うことが多そうなので、lib/src/util/path_util.dartにPathUtilクラスを作成することにしました。
まず、1のカレントディレクトリがDartProjectの中なのかを確認したいです。DartProjectであるかどうかはpubspec.yamlを見ることでわかります。
ここで、DartProjectの中のどこにいてもコマンドは実行したいので、今いる場所でpubspec.yamlを探して、なければ親の階層にあるかを探していく処理を再帰的に行うことで確認することができます。
再帰的に確認していくことで最終的にはOSのルートディレクトリに辿り着き、それでもpubspec.yamlがなければ(まあ、普通ルートディレクトリにpubspec.yamlはないだろうけど)、カレントディレクトリはDartProject内にはいないと言えます。
import 'dart:io';
final currentDirectory = Directory.current;
class PathUtil {
Future<Directory?> findDartProjectRoot({Directory? dir}) async {
dir ??= currentDirectory;
var pubspecFile = File('${dir.path}/pubspec.yaml');
if (await pubspecFile.exists()) {
return dir;
} else {
if (dir.parent.path == dir.path) {
return null;
}
return findDartProjectRoot(dir: dir.parent);
}
}
}
上記がうまく動いているかを確認するためにFullCoverageのrunコマンドを下記に変更します。また、PathUtilはコンストラクタインジェクションで渡してあげましょう。後からテストコードを書くときに役にたちます。
Future<int> run() async {
final path = await pathUtil.findDartProjectRoot();
if (path != null) {
print(path);
} else {
print('No dart project found!!!!');
}
return 0;
}
findDartProjectRootは現在DartProject内にいればそのパスを返し、いなければnullを返してくれるのでnullチェックをすることでDartProjectにいるかどうかをチェックできます。
miyaji@mac src % pwd
/Users/miyaji/DartProjects/dart_test_tools/lib/src
miyaji@mac src % dart_test_tools
Directory: '/Users/miyaji/DartProjects/dart_test_tools'
miyaji@mac src % cd
miyaji@mac ~ % pwd
/Users/miyaji
miyaji@mac ~ % dart_test_tools
No dart project found!!!!
実際に確認してみると、dart_test_toolsのlib/srcにいるときは'/Users/miyaji/DartProjects/dart_test_tools'
を出力し、ホームディレクトリにいるときはNo dart project found!!!!
と出力されてました。これで「1 現在のプロジェクトがDartProjectであるかを確認する。」はクリア🙆♂️です。
2 現在のプロジェクトのlib配下のファイルのパスを全て取得する。
次にlib配下の全てのファイルのパスを取得します。
PathUtilクラスにfindSourcePathsを追加します。
Future<List<String>?> findSourcePaths({String? sourceDirPath}) async {
final projectRoot = await findDartProjectRoot();
if (projectRoot == null) {
return null;
}
final srcDir = Directory('${projectRoot.path}/${sourceDirPath ?? 'lib'}');
final srcPaths = <String>[];
await for (var entity in srcDir.list(recursive: true)) {
if (entity is File) {
srcPaths.add(entity.path.replaceFirst('${srcDir.path}/', ''));
}
}
return srcPaths;
}
先ほど実装したfindDartProjectRootでプロジェクトのルートディレクトリを取得します。もちろんDartProjectではない場合はnullを返します。
次にソースコードが置かれているファイル以下を探索します。基本的にlib/配下を探索すればいいですが、lib/以外の場所にソースコードがある場合も想定してパスを引数で受け取れるようにしました。
FullCoverageのrunメソッドを変更して確認してみます。
Future<int> run() async {
final path = await pathUtil.findSourcePaths();
if (path != null) {
print(path);
} else {
print('No dart project found!!!!');
}
return 0;
}
dart_test_toolsの中で実行してみると
$ dart_test_tools;
[dart_test_tools.dart, src/util/path_util.dart, src/commands/full_coverage.dart, src/dart_test_tools_base.dart, src/command_runner.dart]
これまでに作成したソースコード達が出力されました。
これで「2 現在のプロジェクトのlib配下のファイルのパスを全て取得する。」もクリア🙆♂️です。
3 元々用意してあるcoverage_test.dartの中身に2のファイルパスを埋め込む。
まずcoverage_test.dartを用意しましょう。これは生成するcoverage_test.dartの雛形になります。
/// *** GENERATED FILE - ANY CHANGES WOULD BE OBSOLETE ON NEXT GENERATION *** ///
/// Helper to test coverage for all project files
// ignore_for_file: unused_import
void main() {}
次に先ほど取得したlib配下のファイルのパスをimport 'pacakge:<PACKAGE_NAME>/<FILE_PATH>
のように変換したいのでパッケージネームをpubspec.yamlから取得するgetPackageメソッドと追加したcoverage_test.dartを読み込むreadCoverageTestFileメソッドををPathUtilに追加します。
Future<String?> getPackageName() async {
final projectRoot = await findDartProjectRoot();
if (projectRoot == null) {
return null;
}
final pubspecContent =
await File('${projectRoot.path}/pubspec.yaml').readAsString();
final packageName =
RegExp(r'name: (.+)').firstMatch(pubspecContent)?.group(1);
return packageName;
}
String? readCoverageTestFile(
{String assetPath = 'asset/dart/coverage_test.dart'}) {
final coverageTestFile = File(assetPath);
if (coverageTestFile.existsSync()) {
return coverageTestFile.readAsStringSync();
} else {
return null;
}
}
次にFullCoverageにreadCoverageTestFileとformatPathToImportSentenceを追加し、runメソッドを変更します。
Future<int> run() async {
final packageName = await pathUtil.getPackageName();
if (packageName == null) {
return -1;
}
final paths = await pathUtil.findSourcePaths();
if (paths == null) {
return -1;
}
final imports =
paths.map((path) => formatPathToImportSentence(path, packageName));
final coverageTestFile = readCoverageTestFile();
if (coverageTestFile == null) {
return -1;
}
final output = coverageTestFile.replaceFirst(
'void main() {}', '${imports.join('\n')}\n\nvoid main() {}');
print(output);
return 0;
}
String formatPathToImportSentence(String path, String packageName) {
return "import 'package:$packageName/$path';";
}
readCoverageTestFileは先ほど作成したcoverage_test.dartを読み込んでいます。
formatPathToImportSentenseは相対パスとパッケージネームを受け取ってimport文に変換しています。
runメソッドでは読み込んだcovreage_test.dartのmain関数の前にimport文を挿入する処理をして出力しています。
$ dart_test_tools
/// *** GENERATED FILE - ANY CHANGES WOULD BE OBSOLETE ON NEXT GENERATION *** ///
/// Helper to test coverage for all project files
// ignore_for_file: unused_import
import 'package:dart_test_tools/dart_test_tools.dart';
import 'package:dart_test_tools/src/util/path_util.dart';
import 'package:dart_test_tools/src/commands/full_coverage.dart';
import 'package:dart_test_tools/src/dart_test_tools_base.dart';
import 'package:dart_test_tools/src/command_runner.dart';
void main() {}
実行してみると作成したいファイルの中身が出力できました。これで「3 元々用意してあるcoverage_test.dartの中身に2のファイルパスを埋め込む。」はクリア🙆♂️です。
4 test/coverage_test.dartを生成する。
最後に先ほど出力した内容をファイル出力するように変更します。
FullCoverageクラスにwriteCoverageTestFileを実装します。
// ファイル出力
void writeCoverageTestFile(String content, String projectRootPath) {
final coverageTestFile = File('$projectRootPath/test/coverage_test.dart');
coverageTestFile.writeAsStringSync(content);
}
runメソッドを下記のように変更します。
Future<int> run() async {
final packageName = await pathUtil.getPackageName();
if (packageName == null) {
return -1;
}
final paths = await pathUtil.findSourcePaths();
if (paths == null) {
return -1;
}
final imports =
paths.map((path) => formatPathToImportSentence(path, packageName));
final coverageTestFile = pathUtil.readCoverageTestFile();
if (coverageTestFile == null) {
return -1;
}
final output = coverageTestFile.replaceFirst(
'void main() {}', '${imports.join('\n')}\n\nvoid main() {}');
final projectRoot = await pathUtil.findDartProjectRoot();
if (projectRoot == null) {
return -1;
}
writeCoverageTestFile(output, projectRoot.path);
return 0;
}
実行するとtest/coverage_test.dartが生成されました。これで「4 test/coverage_test.dartを生成する。」もクリア🙆♂️です。
これで今回実装したい機能が完成したので次の章からPub.devに上げていきます。
Pub.devに上げる前にやること
Preparing to publishに準備することがまとまられています。
- Licenceファイルの用意
- 必要があればファイルの圧縮
- GoogleAccountの用意
- ドキュメントファイルの整備(README.md,CHANGELOG.md,The pubspec)
- VerifiedPublisherAccount(必須ではない)
- dart pub publish --dry-runで問題がないかを確認
Licenceファイルの用意
公式によるとBSD 3-clauseライセンスを推奨しているみたいなので、今回はこちらを採用させていただきます。
Licence周りはGPTに聞きながら理解して最終的にはこんな感じになりました。
必要があればファイルの圧縮
圧縮後に100MB以下にする必要があるそうです。
圧縮後のサイズはのちの工程でわかるので一旦触れません。今回は100MBを超えていなかったので特に何もしませんでした。
GoogleAccountの用意
多分みんな持っているので特に解説しません。
ドキュメントファイルの整備
Pub uses the contents of a few files to create a page for your package at pub.dev/packages/<your_package>. Here are the files that affect how your package's page looks:
Pub.devのページの見た目に影響するファイルは3つあるようです。
-
README.md : Pub.devのページのメインコンテンツです。Markdownで書くことができます。Writing package pagesにより詳しい解説があります。
-
CHANGELOG.md : Pub.devのChangelogタブの内容です。主にVersionごとの変更内容を記載します。こちらもMarkdownで書けるようです。
- The pubspec : pubspec.yamlです。Pub.devのページの右側の詳細情報を埋めるのに使われるようです。
今回書いたドキュメントファイルのリンクを下記に置いておきます。
VerifiedPublisherAccountの作成
Verified publisher accountとはPub.devにPackageを公開できるアカウントのことです。これは必須では無く、GoogleAccountがあればPackageの公開は可能のようです。今回はGoogleAccountを利用してPublishしてみようと思います
dart pub publish --dry-runで問題がないかを確認
pub devではdart pubコマンドにpublish用のコマンドがある。
いきなりpublishする前に公開の準備が適切にできているか確認するための--dry-run
オプションが用意されているのでそちらを叩いてみる
dart pub publish --dry-run
dry runの結果
miyasic@mac dart_test_tools % dart pub publish --dry-run
Resolving dependencies... (1.9s)
vm_service 14.1.0 (14.2.0 available)
Got dependencies!
1 package has newer versions incompatible with dependency constraints.
Try `dart pub outdated` for more information.
Publishing dart_test_tools 1.0.0 to https://pub.dev:
├── CHANGELOG.md (<1 KB)
├── LICENSE.txt (1 KB)
├── README.md (1 KB)
├── analysis_options.yaml (1 KB)
├── asset
│ └── dart
│ └── coverage_test.dart (<1 KB)
├── bin
│ └── dart_test_tools.dart (<1 KB)
├── lib
│ ├── dart_test_tools.dart (<1 KB)
│ └── src
│ ├── command_runner.dart (<1 KB)
│ ├── commands
│ │ └── full_coverage.dart (1 KB)
│ └── util
│ └── path_util.dart (1 KB)
├── pubspec.yaml (<1 KB)
└── test
├── coverage_test.dart (<1 KB)
└── src
├── command_runner_test.dart (<1 KB)
├── command_runner_test.mocks.dart (7 KB)
├── commands
│ ├── full_coverage_test.dart (4 KB)
│ └── full_coverage_test.mocks.dart (2 KB)
└── util
└── path_util_test.dart (1 KB)
Total compressed archive size: 6 KB.
Validating package... (1.3s)
Package validation found the following potential issues:
* Please consider renaming /Users/miyasic/Project/dart/dart_test_tools/LICENSE.txt to `LICENSE`. See https://dart.dev/tools/pub/publishing#important-files.
* It's strongly recommended to include a "homepage" or "repository" field in your pubspec.yaml
* /Users/miyasic/Project/dart/dart_test_tools/CHANGELOG.md doesn't mention current version (1.0.0).
Consider updating it with notes on this version prior to publication.
Package validation found the following hint:
* The latest published version is 5.7.0.
Your version 1.0.0 is earlier than that.
Package has 3 warnings and 1 hint.
まず、依存関係の解決が行われている。
Resolving dependencies... (1.9s)
vm_service 14.1.0 (14.2.0 available)
Got dependencies!
1 package has newer versions incompatible with dependency constraints.
vm_serviceの依存関係に対する指摘
vm_serviceについて14.1.0を利用しているが14.2.0が利用できるよと言われている。
こちらはdart pub upgrade
で解消された。
ライセンスファイルの名前に対する指摘
次にLICENSEファイルの名前について指摘がある。
Please consider renaming /Users/miyasic/Project/dart/dart_test_tools/LICENSE.txt to
LICENSE
. See https://dart.dev/tools/pub/publishing#important-files.
素直にLICENSEに変更したら解消された。
repositoryリンクに対する指摘
次にrepositoryかhomepageのlinkをpubspecに記載するのを強く推奨されている。
It's strongly recommended to include a "homepage" or "repository" field in your pubspec.yaml
- # repository: https://github.com/my_org/my_repo
+ repository: https://github.com/miyasic/dart_test_tools
pubspec.yamlにsampleがコメントアウトされて書かれていたので、こちらをレポジトリのURLに変更したら解消された。
Pacakgeバージョンに対する指摘
最後にPackageの最新Versionは5.7.0で0.0.1はそれより低いですよと指摘されています。
The latest published version is 5.7.0.
Your version 0.0.1 is earlier than that.
いやいや、今回初めてPublishするんですけどって思いながらpub.devで検索してみてもやはり同じ名前のPackageは見つかりません。
仕方なくGPTに聞いてみると存在しますよと教えてくれました。
確かにdart_test_toolsはすでに存在していましたが、よくみてみるとUNLISTEDになっているためpub.devの検索に引っ掛からなかったようです。
pub.devの検索にはUnlistedなPackageも含めて検索するチェックボックスがあるため、そちらをオンにして検索する必要があったみたいです。
Package名はpub.dev全体で一意である必要があるので、名前を変えることにします。ちゃんと同じ名前のPackageがないか調べたつもりだったのに悔しい、、🥲
仕方ないので、今回はdart_test_toolsの頭文字をとってdttとします。
ちゃんとpub.devでunlistedなPackageを含めてdttが存在しないか確認しました。
VSCodeの機能を使って全部のdart_test_toolsをdttに置換し、libとbinにあるdart_test_tools.dartもdtt.dartに変更しました。
ついでにGithubのRepository名もdttに変更しました。
再dry runの結果
miyasic@mac dtt % dart pub publish --dry-run
Resolving dependencies...
Got dependencies!
Publishing dtt 0.0.1 to https://pub.dev:
├── CHANGELOG.md (<1 KB)
├── LICENSE (1 KB)
├── README.md (<1 KB)
├── analysis_options.yaml (1 KB)
├── asset
│ └── dart
│ └── coverage_test.dart (<1 KB)
├── bin
│ └── dtt.dart (<1 KB)
├── lib
│ ├── dtt.dart (<1 KB)
│ └── src
│ ├── command_runner.dart (<1 KB)
│ ├── commands
│ │ └── full_coverage.dart (2 KB)
│ └── util
│ └── path_util.dart (1 KB)
├── pubspec.yaml (<1 KB)
└── test
├── coverage_test.dart (<1 KB)
└── src
├── command_runner_test.dart (<1 KB)
├── commands
│ ├── full_coverage_test.dart (4 KB)
│ └── full_coverage_test.mocks.dart (2 KB)
└── util
└── path_util_test.dart (1 KB)
Total compressed archive size: 5 KB.
Validating package... (1.2s)
Package has 0 warnings.
The server may enforce additional checks.
晴れてwarningが0になりました!
またログを見てみると圧縮後のサイズが5KBととなっています。これで100MBの制限はクリアです。今回はCLIツールで、重たいアセットなどを利用しなかったため特に気にせずにクリアできました。
実際に公開してみる
ここまできたら準備OKです。満を持してdart pub publishを叩いてみます。
dart pub publish
実行してみると公開するか?と聞かれるのでもちろんYes
Do you want to publish dtt 0.0.1 to https://pub.dev (y/N)?y
ブラウザでログインして承認してくれとのことなので、
In a web browser, go to https://<省略>
Then click "Allow access".
Waiting for your authorization...
httpsから始まるURLをコピペして、ブラウザに貼り付けるとGoogleログインを求められたので、ログインしてアクセスを許可すると
Authorization received, processing...
Successfully authorized.
Uploading... (2.0s)
Successfully uploaded https://pub.dev/packages/dtt version 0.0.1.
キターーーー🥳🥳🥳🥳
どうやら公開に成功したようです。
こちらのURLにアクセスしてみると
公開されてました!
一旦すでにローカルでactivateされているdttをdeactivateした上でpub.devからactivateしてみました。
miyasic@mac dtt % dvm dart pub global deactivate dtt
Deactivated package dtt 0.0.1 at path "/Users/miyasic/Project/dart/dtt".
miyasic@mac dtt % dtt
zsh: command not found: dtt
miyasic@mac dtt % dvm dart pub global activate dtt
Resolving dependencies...
+ args 2.4.2
+ dtt 0.0.1
+ path 1.9.0
Building package executables...
Built dtt:dtt.
Installed executable dtt.
Activated dtt 0.0.1.
miyasic@mac dtt % dtt full_coverage
miyasic@mac dtt % dvm dart pub global deactivate dtt
Deactivated package dtt 0.0.1.
dttがちゃんと見つかるようになっていますね。
また、1回目のdeactivateではローカルのPathが表示されていますが、2回目のdeactivateではPathが表示されておらずpub.devから取得したものがactivateされていたことがわかります。
おわりに
今回は以前shell scriptを使って実現していたCLIツールをdartで実装して、pub.devに上げるところまでやってみました。dttはdart_test_toolsという割と汎用的な名前をつけているので、他にもいくつかTestを書くときに便利な機能をつけれたらと思います!
2024年は技術発信も頑張ろうと思っているので、記事が参考になった方は記事とGitHubのいいね(スター)とフォローをしていただけると励みになります!
最後まで読んでいただきありがとうございました✨
Discussion