以下ページの転載になります。ご了承ください。
Rustを0から学んでみた〜Part.1〜 struct, impl, テストコードの基礎編(TDD Boot Camp 課題1「閉区間」)
TDDBCの課題をRustで実施し、言語仕様を学んでいこうと思っています。
-
- TDD Boot Camp(TDDBC) – TDDBC仙台03/課題
- TDD Boot Camp(TDDBC) – TDDBC仙台03/課題用語集
[ひとことで言うとこんな記事]
以下2点を学ぶ事ができます
-
- 他の言語でいうClass的なものを実現する方法(struct, impl)
- RustでのTest Code・Unit Testの基礎
[こんなひとにおすすめ]
-
- 「これからRustを勉強していきます!」という方
-
- 「Rustってclassないの?!」と驚いている方
- 初歩的なモジュールとそれに対するテストコードの書き方を知りたい方
[目次]
-
- 準備:cargo new コマンドのみ
-
- 課題:整数の閉区間(Closed Range)
-
- 学んだこと0:前提
-
- 学んだこと1:Rustでのオブジェクトの表現 〜オブジェクトはデータと振る舞いを含む〜
-
- 学んだこと2:Rustのテストコード基礎
-
- 今回の実装の Pull Request
- 次回:TDDBC「課題2:開区間」 実施予定
準備:cargo new コマンドのみ
インストール等の記事は、以下記事をご覧ください。
準備
cargo new tddbc-rust-practice --bin
課題: 整数の閉区間(Closed Range)
今回ご紹介する課題の内容です。
課題1-1
-
- 下端点と上端点を与えて閉区間を生成
-
- 閉区間から下端、上端の取得
- 閉区間から文字列の取得
課題1-2
- 閉区間が指定した整数を含むか(contains)判定
課題1-3
-
- 閉区間が別の閉区間と等しいか(equals)判定
- 閉区間が別の閉区間と接続しているか(isConnectedTo)判定
学んだこと0:前提
mod(モジュール)の準備方法
pushした私のリポジトリのtreeです。
.
├── Cargo.toml
├── README.md
├── src
│ ├── lib.rs ←コレ
│ └── main.rs
└── tests
└── lib.rs
src/lib.rsに準備することでいい感じになるそうです。
基本 Private (例外アリ)
By default, everything in Rust is private, with two exceptions: Associated items in a pub Trait are public by default; Enum variants in a pub enum are also public by default. When an item is declared as pub, it can be thought of as being accessible to the outside world. For example:
// Declare a private struct
struct Foo;// Declare a public struct with a private field
pub struct Bar {
field: i32,
}// Declare a public enum with two public variants
pub enum State {
PubliclyAccessibleState,
PubliclyAccessibleState2,
}引用:Visibility and Privacy – The Rust Reference
traitとenumに関しては例外的にpublicがデフォルトだそうです。
学んだこと1:Rustでのオブジェクトの表現〜オブジェクトはデータと振る舞いを含む〜
Rustでもオブジェクト指向ができます。
参考:オブジェクト指向言語の特徴 – The Rust Programming Language
実際に書いてみたソースコードはこちらになります。(GitHubのページへ飛びます)
GitHub – h0ng0yut0 tddbc-rust-practice/lib.rs
struct(構造体)
メンバ変数定義のようなものですね。「閉区間は下限と上限を持つ」という例が以下です
。
// lib.rs
#[derive(Clone)]
pub struct ClosedRange {
pub lower: i8,
pub upper: i8,
}
上記「基本 Private (例外アリ)」で Private Public の話をしましたが、 structのメンバもデフォルトpublicでよくね? とか思うんだけどどうなんだろう。。。
また、#[derive(Clone)]を記述することにより、同じ値を持った構造体をcloneすることができる。(cloneしないで普通に代入すると、Rustのメモリ管理方法によりスコープ外とみなされますからね〜)
// main.rs
let sample_closed_range = closed_range::ClosedRange::new(1, 5);
let sample_closed_range_same = sample_closed_range.clone();
impl(実装)
structの持つ振る舞いを定義しています。
#[derive(Clone)]
pub struct ClosedRange {
pub lower: i8,
pub upper: i8,
}
impl ClosedRange {
pub fn new(lower: i8, upper: i8) -> ClosedRange {
match (lower, upper) {
(lower, upper) if lower > upper => Err("下限と上限の値が不正です".to_owned()),
_ => Ok(ClosedRange { lower: lower, upper: upper }),
}
.unwrap() // 生成時にErrの場合panic!させちゃう
}
pub fn to_string(&self) -> String {
format!("[{},{}]", self.lower, self.upper)
}
}
今回 new と to_string を定義させていただきました。impl自体にpubを書かずに、fnにつけるらしいです。
学んだこと2:Rustのテストコード基礎
実際にこの課題で書いてみたソースコードは以下になります。
GitHub – h0ng0yut0 tddbc-rust-practice/lib.rs
テストコードの記述”場所” 2種類
1. 同じファイル内にテストを記述する
.
├── Cargo.toml
├── README.md
└── src
├── lib.rs ←ココ(本機能と同じ場所)
└── main.rs
以下のように記述できます。
pub mod closed_range {
// 実際に定義したstructやimplを実装
#[cfg(test)] // 【1】テストコード記載場所を示す
mod tests {
use super::*; // 【2】外部モジュールで定義したものを利用できるようにする
#[test] // 【3】テストをfnで記述していく
fn test1() {
}
#[test] // 【3】テストをfnで記述していく
fn test2 (){
}
}
}
利点としては、 実装箇所と同じ場所にテストコードを記述することで、小さいサイズの実装であれば見通しが良い というところでしょうか。
はじめこちらの場所で実装していたのですが、本機能の実装を見通しよくするために、別の場所でテストコードを管理することとしました。
2. tests ディレクトリ内に記述する
.
├── Cargo.toml
├── README.md
├── src
│ ├── lib.rs
│ └── main.rs
└── tests
└── lib.rs ← コレ
以下のように記述できます。
// 【1】各々ライブラリ等をインポートする必要がある
extern crate tddbc_rust_practice;
use tddbc_rust_practice::closed_range::ClosedRange;
#[test] //【2】テストをfnで記述していく
fn test1(){
}
#[test] //【2】テストをfnで記述していく
fn test2(){
}
1個目との違いは以下の点です。
必要なライブラリはインポートする必要がある (【1】)
testsディレクトリ内にあるソースコードはすべてテストのためにあると認識されるので、#[cfg(test)] の記載は不要
利点としては、同じ場所にテストコードを書かないので 本実装の見通しが良いこと でしょうか??
テストコードのアサーションと書き方
assert_eq!(left, right)
left == right になる。以上。(equal)
assert_ne!(left, right)
left != right になる。以上。(not equal)
assert!(hoge)
hogeはtrueになる。以上。
#[should_panic] でエラー確認
今回の例でいうと、下限と上限の入力値がおかしい場合にエラーを引き起こすように設計しているので、テストコードは以下のようになります。
実装
impl ClosedRange {
pub fn new(lower: i8, upper: i8) -> ClosedRange {
match (lower, upper) {
(lower, upper) if lower > upper => Err("下限と上限の値が不正です".to_owned()),
_ => Ok(ClosedRange { lower: lower, upper: upper }),
}
.unwrap() // 生成時にErrの場合panic!させちゃう
}
}
テストコード
#[test]
#[should_panic(expected = "下限と上限の値が不正です")]
fn new_error() {
// 下限、上限の入力が不正な場合
let lower = 5;
let upper = 1;
ClosedRange::new(lower, upper);
}
以上のように書けば、どんな内容のpanic!かもテストできますね。
今回の実装の Pull Request
課題1:閉区間 by h0ng0yut0 · Pull Request #1 · h0ng0yut0/tddbc-rust-practice
[これから読んでみたい本]
https://amzn.to/34pexMw
最後までお読みいただき、誠にありがとうございます。様々なことを学んでいきたいと思っていますので、内容やソースコード等のアドバイスいただけると幸いです。