Rustを0から学んでみた 〜Part.1〜 struct, impl / テストコードの基礎 編(TDD Boot Camp 課題1「閉区間」)

「TDDBCの課題をRustで実施し、言語仕様を学んでいこう!」というシリーズの記事です。


[ひとことで言うとこんな記事]

以下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 コマンドのみ

インストール等の記事は、以下記事をご覧ください。

 learn-rust-part0-introduction-and-setup-thumb
Rustを0から学んでみた 〜Part.0 準備/概要 編〜

準備

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

traitenumに関しては例外的にpublicがデフォルトだそうです。

学んだこと1:Rustでのオブジェクトの表現〜オブジェクトはデータと振る舞いを含む〜

参考:オブジェクト指向言語の特徴 - The Rust Programming Language

他言語で使われるいわゆるClassというものを実現することができます。

実際に書いてみたソースコードはこちらになります。(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)
  }
  
}

今回 newto_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

次回:TDDBC「課題2:開区間」 実施予定

今回は、struct, impl, テストコード等を学べました。次回の課題は何が学べるのか楽しみです。


[これから読んでみたい本]


一緒に様々なことを学んでいく仲間を募集しています。

このサイトのお問い合わせなどからご連絡いただけると幸いです。