2004年7月16日

DBUnitでデータベーステスト

 業務でアプリケーションプログラミングを行っている人で、データベースを使わない案件はまれでしょう。
ほとんどがデータベースに顧客データを格納したり、マスターデータを引っ張ってきたりするはずです。

そんなデータベースとのやり取りで、SQL文を書いたが、それがほんとうに合っているのか?、
単体テスト時にテストデータを毎回手動でデータベースに入れては、
プログラムで引っ張ってきた値と等しいかをチェックするのはばかばかしい、
データを更新してはみたが、ほんとうに更新されているのかを確認しないといけない、
なんて作業はとてもめんどくさくて憂鬱な作業ですよね。

そんな時に威力を発揮するのが、DBUnitという、
データベースに関係する単体テストを自動で行ってくれるツールです。
もちろん、どんなテストケースがあるのかは自分で書かなければいけませんが、
意図しているデータや、更新されたはずのデータはすべて外部のXMLファイルに書くため、
プログラマとテスト実施者が別々に作業することも可能です。

DBUnitは開発環境に依存しませんので、
EclipseなどのIDEを使っているかテキストエディタを使っているかは問題にしません。

特徴

 DBUnitの特徴は以下のようなものです。

  • テスト用のテーブルの自動生成と自動削除機能
  • テストデータの自動insertと自動delete機能
  • データベース内を直接見なくても、データの比較を行ってくれる
  • テストデータとテストコードの分離(テストデータはXMLファイルとして記述できる)
  • DTDをエクスポートできる(多少のプログラミングが必要[5処理くらい])

+ DBUnitはこちらからダウンロードできます。
SouceForgeでDBUnitをダウンロードlinkext

 DBUnitはテストデータを外部のXMLファイルに記述することができます。もちろん、プログラム中に埋め込むこともできますが、テストコードとテストデータを分離して開発すれば、プログラマーとテスト担当者に作業を分割することもできます。

XMLBuddy

 テストデータを記述するこの作業はルーチンワークになりますので、テストエディタで処理するのは次第に苦痛になってくるでしょう。そんな時におすすめなのが、EclipseのプラグインであるXMLBuddyです。XMLBuddy はXMLエディタとしてEclipseプラグインで提供されています。記述の補佐や要素をツリー形式で表示してくれたりします。

+ XMLBuddyはこちらからダウンロードできます。
XMLBuddyのダウンロードlinkext

DBEdit

 これらをEclipseで使用する場合はついでにDBEditもインストールすると良いでしょう。DBEditはEclipseプラグインでデータベースの中身をEclipseから見ることができるツールです。テーブルの関連などもボタン一発で参照できますし、データの追加・変更・削除もEclipse上からできるようになります。

+ DBEditはこちらからダウンロードできます。
DBEditのダウンロードlinkext

まとめ

 DBUnitを使ったテストは最初はデータベースの設定やDTDの読み込みなどの手間が発生します。これはこれからくる簡単な単体テストの甘い蜜の下準備としてがんばって設定してください。この設定が終われば後は、XMLファイルにテストデータを記述してDBUnitを実行するだけです。テーブルを自動で作成してくれて、XMLファイルに書かれたテストデータを自動でinsertしてくれ、自分の書いたselect文と比較して間違っていいないか確認してくれます。

 このツールの恩恵は、単体テストではもちろん、納品後の保守段階でバグが出た場合にも受けることができます。納品後というのは、実際にシステムが稼動していることがしばしばあります。そんな時に、実データに手を入れてテストを行うのは至難の業です。DBUnit を使うと、多少テストが簡単になるかもしれません。

Tips Tips
DBUnitを使うことでテストデータを自動でinsertしてくれ、insertしたデータは単体テスト終了後に自動的に削除するという設定もできます。

 今まで複雑で、めんどくさかったデータベースの単体テストがこのDBUnitを使って簡単になるといいなと思います。

DBUnit で使う表定義の DTD ファイルを自動生成する

 DBUnitには表定義を DTD ファイルとして作成する機能があります。少しソースコードを書かないといけないので、サンプルコードを載せておきます。間違ってたらもうしわけないです。   
DTDExporter.java

import java.io.*;
import java.sql.*;

import org.dbunit.database.DatabaseConnection;
import org.dbunit.database.IDatabaseConnection;
import org.dbunit.dataset.xml.FlatDtdDataSet;


/**
 * データベースのスキーマから DTD を吐き出すクラス
 * 接続するデータベースへの情報、エクスポートするファイル名は適宜変更する
 * @author hamasyou
 */

public class DTDExporter {

    /** エクスポートするファイル名 */
    private static final String EXPORT_FILENAME = "tables.dtd";

    /** スキーマ名 */
    private static final String SCHEMA = "schema";

    /** 接続するデータベースへの情報(PostgreSQL用)情報適宜変更すること */
    private static final String JDBC_DRIVER = "org.postgresql.Driver";
    private static final String JDBC_URL = "jdbc:postgresql://YOUR_HOST/DATABASE_NAME";
    private static final String JDBC_USER = "postgres";
    private static final String JDBC_PASS = "*******";

    /** 接続するデータベースへの情報(MySQL用) */
//    private static final String JDBC_DRIVER = "org.gjt.mm.mysql.Driver";
//    private static final String JDBC_URL = "jdbc:mysql://YOUR_HOST/DATABASE_NAME";
//    private static final String JDBC_USER = "mysql";
//    private static final String JDBC_PASS = "*******";

    public static void main(String[] args) throws Exception {
        if (args.length < 1) {
            System.out.println("使い方: java DTDExporter TABLE_NAME [TABLE_NAMES]");
            System.exit(-1);
        }

        Class.forName(JDBC_DRIVER);
        IDatabaseConnection con = 
                new DatabaseConnection(DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASS), SCHEMA); 
        FlatDtdDataSet.write(con.createDataSet(args), new FileOutputStream(EXPORT_FILENAME)); 
    }
}

 これを実行してできたDTDファイルを、XMLBuddy で利用することで、テストデータを XMLBuddy で作成するときに補完機能が効くようになります。XMBuddy で作成したテストデータ記述用ファイルの先頭に次の記述を行います。

<?xml version="1.0"?>
<!DOCTYPE dataset SYSTEM "tables.dtd">

これでこれでいくらかテストデータを作成する効率が上がると思います。

Tips Tips
XMLBuddyで補完を使う場合、「.xml」ファイルのエディターを「XMLBuddy」にしておく必要があります。Eclipse メニューの 「ウィンドウ」→「設定」→「ワークベンチ」→「ファイルの関連付け」 で .xml のデフォルトエディターを XMLBuddy に設定しておくと便利です。

ハマりそうなポイント

例外に対応する

XML ファイルの読み込み例外が発生した場合

 XMLファイルの読み込み時に以下のような例外が発生した場合の対応方法です。  
org.dbunit.dataset.DataSetException: Line 2: 基本 URI を使用せずに、相対 URI "tables.dtd" を解決することはできません。
 XML ファイルの DTD 読み込み部分を書き換えてみてください。  
<?xml version="1.0"?>
<!DOCTYPE dataset SYSTEM "file:///C:/Documents and Settings/hamasyou/test/tables.dtd">

上のように URI の部分を絶対パスで書いてみてください。環境に依存しますが、テストコード用ですので OK としましょう。

データベースコネクションが取得できない場合

 データベースコネクションを取得できない場合や、DTD を吐き出せない場合に下記の例外が出たら、コネクション取得時にスキーマ名を指定してみてください。

org.dbunit.database.AmbiguousTableNameException

Operationの種類

 DBUnitの実行時に呼び出される getDataSet() メソッドの挙動を変更できる Operationの種類です。  
Operation動作
INSERT指定したデータセットをテーブルに挿入します。テーブル内で同じ主キーとなるデータがすでに存在する場合は、エラーになります。外部キーなどの参照整合性を保つようにデータセットの順序を指定する必要があります。
UPDATE指定したデータセットの同じ主キーとなるデータを上書きします。上書きするデータが存在しない場合エラーになります。テストデータ以外が存在する場合、データの上書きに注意してください。
DELETE指定したデータセットとマッチするものだけを削除します。データセットに含まれていないデータは、削除されません。
DELETE_ALL指定したデータセットに存在するテーブルのレコードすべてを削除します。テーブル自体は削除されません。データセットの逆順にデータが削除されていきます。
CLEAN_INSERT指定したデータセットに存在するテーブルに対して、DELETE_ALLを行った後、INSERTを行います。データセットに含まれるデータのみでテストしたい場合に使います。既存のデータはすべて削除されてしまいます。
REFRESH指定したデータセットの主キーにマッチするデータを更新します。更新するデータがない場合は、挿入されます。ほとんどのデータベーステスト時に使用できますが、既存データの更新にだけは注意する必要があります。
TRANCATE_TABLE指定したデータセットに含まれるテーブルが削除されます。テーブルに格納されているデータもすべて削除されるので、注意してください。
NONE何も行わない処理です。

動作に関しての注意点

Warning Warning
保守段階で、DBUnitを使う場合、既存のデータの更新や削除に注意してください。間違ってもテーブルの削除(TRANCATE_TABLE)などしないように!
Information Information
  • データセットで、DEFAULT 値の設定してある列に DEFAULT 値を入れる方法がみつかりません・・・
  • シーケンス値が主キーになるようなデータの挿入の方法がわかりません・・・
  • テーブルの列が Number などの数値型の場合、比較時に文字として認識されてしまう
  • Timestamp 型は、正確に同じデータを入れておかないと、比較に失敗する

特殊な値をテストデータとして入力するには?

現在時刻をテストデータとして入力する方法

 テストデータをXMLに書く場合、Timestamp型の列には現在日時を入れたいときがある。そんな場合は、ReplacementDataSet を使用する。データセットに例えば次のように書いておく。  
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE dataset SYSTEM "tables.dtd">
<dataset>
    <TABLENAME TODAY="[SYSDATE]" ID="1"/>
    <TABLENAME TODAY="2004-06-10" ID="2"/>
</dataset>

 [SYSDATE] の部分を ReplacementDataSet#addReplacementObject() を使って置き換えることで、任意の値でテストすることができます。

テストクラス

protected IDataSet getDataSet() throws Exception {
    ReplacementDataSet dataSet =
    new ReplacementDataSet(new FlatXmlDataSet(new FileInputStream("dataset.xml")));
    dataSet.addReplacementObject("[SYSDATE]",
    new Timestamp(System.currentTimeMillis()));
    return dataSet;
}

テストデータに null を入れる方法

 上記「現在日時をテストデータに使用する」と同様の方法が使える。  
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE dataset SYSTEM "tables.dtd">
<dataset>
    <TABLENAME TODAY="[NULL]" ID="1"/>
    <TABLENAME TODAY="2004-06-10" ID="2"/>
</dataset>

 [NULL]の部分をReplacementDataSet#addReplacementObject() を使って置き換えることで、任意の値でテストすることができます。

テストクラス

protected IDataSet getDataSet() throws Exception {
    ReplacementDataSet dataSet = new ReplacementDataSet(new FlatXmlDataSet(new FileInputStream("dataset.xml")));
    dataSet.addReplacementObject("[NULL]", null);
    return dataSet;
}

テストデータに ランダムな値を入れる方法

 上記「現在日時をテストデータに使用する」の応用でいけると思います。  
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE dataset SYSTEM "tables.dtd">
<dataset>
    <TABLENAME TODAY="[RANDOM]" ID="1"/>
    <TABLENAME TODAY="2004-06-10" ID="2"/>
</dataset>

 [RANDOM] の部分を ReplacementDataSet#addReplacementObject() を使って置き換えることで、任意の値でテストすることができます。

テストクラス

protected IDataSet getDataSet() throws Exception {
    ReplacementDataSet dataSet = new ReplacementDataSet(new FlatXmlDataSet(new FileInputStream("dataset.xml")));
    dataSet.addReplacementObject("[RANDOM]""" + new Random().nextInt());
    return dataSet;
}

テストコードサンプル

 DBUnitのサンプルコードです。感じだけでも大体つかめてもらえたらと思います。  
テストコードサンプル

package jp.dip.xlegend.domain.test;

import java.io.*;
import junit.framework.*;
import org.dbunit.*;
import org.dbunit.database.*;
import org.dbunit.operation.*;
import org.dbunit.dataset.*;
import org.dbunit.dataset.xml.*;
import jp.dip.xlegend.domain.Form;


/** テストクラス */
public class FormTest extends DatabaseTestCase {
    private IDatabaseConnection connection;

    protected void setUp() throws Exception {
        super.setUp();
        connection = getConnection();
    }

    protected void tearDown() throws Exception {
        connection.close();
        super.tearDown();
    }

    protected IDatabaseConnection getConnection() throws Exception {
        return new DatabaseConnection(getJDBCConnection(), "SCHEMA");
    }

    protected IDataSet getDataSet() throws Exception {
        ReplacementDataSet dataSet = new ReplacementDataSet(new FlatXmlDataSet(new FileInputStream("dataset.xml")));
        dataSet.addReplacementObject("[SYSDATE]"new Timestamp(System.currentTimeMillis()));
        return dataSet;
    }

    protected DatabaseOperation getSetUpOperation() throws Exception {
        return DatabaseOperation.REFRESH;
    }

    protected DatabaseOperation getTearDownOperation() throws Exception {
        return DatabaseOperation.DELETE;
    }
        
    /**
     * データベースコネクションを返します。
     * @param autoCommit    自動コミット(true/自動コミットOn, false/自動コミットOff)
     */

    private Connection getJDBCConnection(boolean autoCommit) throws Exception {
        Class.forName(System.getProperty("test.jdbc.driver"));
        String url = System.getProperty("test.jdbc.url");
        String user = System.getProperty("test.jdbc.user");
        String pass = System.getProperty("test.jdbc.pass");
        Connection con = DriverManager.getConnection(url, user, pass);
        con.setAutoCommit(autoCommit);
        
        return con;
    }
    

    /*=====================================*/
    /* テストケース */
    /*=====================================*/


    /**
     * フォームを期待通りに取得できるかどうかのテスト
     */

    public void testGetData() throws Exception {

        Form form = Form.getData();
        

        // 期待するデータを読み込む
        IDataSet expectedDataSet = new FlatXmlDataSet(new FileInputStream("expect-data.xml"));
        ITable expectedTable = expectedDataSet.getTable("FORM");
                

        // アプリケーションID、フォームID、フォーム名が一致した場合同じ物とみなす
        assertEquals("アプリID", expectedTable.getValue(0, "APP_ID"), form.getAppId());
        assertEquals("フォームID", expectedTable.getValue(0, "FORM_ID"), form.getFormId());
        assertEquals("フォーム名", expectedTable.getValue(0, "FORM_NAME"), form.getFormName());
    }
}

テストデータを Excel で作る

 テストデータを Excel で作ることができます。Excelで作ったデータを読み込むには、XlsDataSet というクラスを使います。このクラスは、内部で Jakarta POI を使っています。
Information Information
POI とは、Javaから Microsoft Word や Microsoft Excel を扱うためのライブラリです。Jakarata POIプロジェクトでは、Microsoft OLE 2複合ドキュメント形式に基づいた様々なファイル形式を Pure Java で取り扱うためのAPI群から成り立つプロジェクトです。 Jakarta POI のダウンロードはこちらlinkext

 Excelでどのようにテストデータを作るかというと、シートごとにテーブルに挿入するデータを作成します。シート名には、挿入するテーブル名をそれぞれ書きます。シートの最初の行に、テーブルのカラム名(列名)を並べて書きます。2行目から挿入するデータを書き並べます。下はサンプル画像です。
 
Excelサンプル

 Excel のセルの書式設定によって、XlsDataSet での値の取り扱い方が違います。数値は BigDecimal 型、日付は Date 型、文字列は String 型で扱われます。  
Tips Tips
数字を文字列として扱いたい場合は、セルの書式設定を「文字列」に設定します。文字列として扱われていれば、Excel2003 であればセルの左上に緑の三角が現れます。

 現時点では、POI が未熟なせいか、まだまだ不安定な部分もあります。(特定の列名の後に空白が入ってしまうなど)それでも、Excelを使ってテストデータを作れるのは、非常に強いと思います。

データセットをSQLを使って取得する

 QueryDataSet クラスの addTable メソッドを使うと、SQL 文を使ってデータセットを作り出すことができます。  
QueryDataSet dataSet = new QueryDataSet(getConnection());
dataSet.add("TABLE_NAME""select * from TABLE_NAME where ID = '1'");
 データベース接続からデータセットを取得する方法もあります。  
getConnection().createDataSet()

Oracle の Timestamp 型が Insert できない

WARNING - CREATE_DATE data type (1111, ‘TIMESTAMP(6)’) not recognized and will be ignored. See FAQ for more information.
 こんな警告が表示されるのは、データ型にデフォルトのJDBC 型が使われているのが原因です。例えば Oracle を使っている場合は、次のようにしてデータ型を Oracle のものに指定してやることで回避できます。  
IDatabaseConnection con = 
                new DatabaseConnection(DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASS), SCHEMA); 
DatabaseConfig config = con.getConfig();
config.setProperty(DatabaseConfig.PROPERTY_DATATYPE_FACTORY, new OracleDataTypeFactory());

 このように設定を指定することで、データベース独自の型を使えるようになります。

効率の良いテストにするには

 DBUnitを使って効率のよいテストを行うには、下の3点に気をつけることで効率があがると思います。

  • 自分専用のデータベースを利用する
  • テスト終了後にデータを削除しない
  • 接続処理などを一元管理する

自分専用のデータベースを持つ

 DBUnitのひとつの弱点に、データベースの状態にテストが依存してしまう>いう点があげられます。これは、複数の開発者が同じデータベースを使ってテストを行っていると顕著になってきます。

 そこで、データベースの状態にできるだけ依存しないテストを行うために、自分専用のデータベースを利用する方法があります。最近では、HSQLDBlinkextMckoilinkext といった、ポータブルデータベースがありますので、簡単に自分専用のデータベースを作ることができると思います。

 この場合の注意点は、最新の DDL を使って自分のデータベースを作らないと、本番データベースとバージョンが違うものができてしまうということです。

[参考]
+ HSQLDB - *HSQLDlinkext
+ Mckoi によるポータブルデータベース生活linkext

テスト終了時にデータを削除しない

 自分専用のデータベースを使えば心配はないのですが、開発者共通のデータベースを使ってテストしていた場合、間違ってすべてのデータを消してしまうということがあります。DBUnitの場合、コマンドをひとつまちがえるだけで簡単にデータがすべて消えてしまいます。

 こういった間違いを起こさないために、テスト終了時のデータは残しておくのがいいと思います。デバッグ時に実際のデータを確認するときにもつかえることですし。

Warning Warning
TRANCATE_TABLE を行うと、テストに使ったテーブルが削除されてしまいます。本番で使っているデータベースでは、必ず確認して、実行しないようにしましょう。

接続処理を一元管理する

 DBUnitを使ってテストケースを書く場合、DatabaseTestCase クラスを継承して作り始めます。このクラスでは、データベースコネクションを取得するメソッドとデータセットを取得するメソッドをオーバーライドしなければなりません。また、テスト開始時のオペレーションやテスト終了時のオペレーションも、適宜オーバーロードしなければ、デフォルトでは「何もしない」設定になっています。

 毎回これらの処理を書くのは、非常にめんどくさいです。そこで、DatabaseTestCase を継承し、デフォルトの実装を行って独自テストケースクラスを作成し、そのクラスを個々のテストケースに継承させます。

参考文献:

+ DBUnit、DBEdit、XMLBuddyをつかったデータベースプログラミングはこちらが参考になります。
JavaPRESS34(技術評論社)

+ DBUnitの本家Webサイトです。膨大な情報があります。
DBUnit

+ テスティングモジュールのJUnitも参考になります。
JUnitイン・アクション
ビンセント マソル テッド ハスティード Vincent Massol Ted Husted クイープ


おすすめ平均 
JUnitやその派生フレームワークの使用法解説

Amazonで詳しく見る
   by G-Tools

+ DbUnitに関して、分かりやすく解説されています。
DbUnit (Stack*)



▼ この記事に関係のあるサイトはこちら

TrackbackURL:【http://hamasyou.com/cgi-bin/mt-tb.cgi?tb_id=208】
他の記事も読んでいきませんか?

最後までお読みいただいてありがとございます。フィードバックお待ちしています。

Comments

津島と申します。
突然のメールすみません。。
質問がありメールさせて頂いております。
GWもなく働いているので、勝手なお願いですが聞いてやって下さい。
Excelのインポートがありますが、複数テーブルを同時に取り込むことは可能でしょうか?
シートを分けて試して見ましたがダメでした。
何かアドバイスがありましたら是非お願い致します。

Posted by: 津島 at 2005年05月05日 19:55

返信遅くなって申し訳ありません。

テーブルのインポートはシート毎に行われるので、
シートを分けてテーブルを指定してやればいけるはずですけどね。

僕が試したときには、シートを分けることでインポートができましたが、テーブル名によってインポートが正常に行われない場合もありました。POIのバグかもしれません。

答えになっていませんが、申し訳ないです。

Posted by: 管理人 at 2005年05月09日 22:49

初めまして。最近DBUnitでテストを始めたものです。

POIのバグで時々期待値に空白が入ってしまうことがあります。その後修正させることができないことが多く、
EXCELでテストデータを運用することはあきらめかけているのですが、みなさまどうお考えですか?

Posted by: kom at 2005年05月10日 11:34

POIで登録した期待値に空白が入ってしまうのは、いまだどうしようもないのですね(泣)

POIのバグを修正して使うか、EXCELでテストデータを運用するのをあきらめるかですね。

私なら、時間があればPOIを修正します。EXCELでデータ管理が出来ると楽ですからね。

Posted by: 管理人 at 2005年05月11日 21:49

通りすがりで失礼いたします。
「Oracle の Timestamp 型が Insert できない」の項、参考にさせていただきます。
エクスポートしたXMLを読み込んでCLEAN_INSERTしようとした際、Longがどうとかいうエラーが出たので、この方法を試してみます。
「テストデータに ランダムな値を入れる方法」というのも面白そうです。

Posted by: 通りすがり at 2007年04月22日 00:47
TrackBack
ユニットテスト
引用: 僕はテストが苦手です。 たまにバグが多いと指摘されることあります。 これではイカンということで最近ユニットテストをするようにしています。 もろリファクタリングの影響ですねw いきなりテスト駆動開発ってのは難しいのでDBUnitを使ってデータベース回りのテストを...
ぶろぐ名: グラスオニオン日記
日時: 2006年01月17日 09:59
コメントを投稿する











日本語を入力しないとエラーになります。



authentication
すべて 小文字の英語です。
※ 表示される画像に記述されている文字を入力してください。
powered by Image Verification

送信情報を保存しますか? 
HOMEに戻る
月別書評
本の種類
同じカテゴリ内の記事
今日のおすすめ
[24時間365日] サーバ/インフラを支える技術 ~スケーラビリティ、ハイパフォーマンス、省力運用
[24時間365日] サーバ/インフラを支える技術 ~スケーラビリティ、ハイパフォーマンス、省力運用
実際の運用に基づいたインフラ構築の実践ノウハウが満載
最近買った本
オブジェクト指向入門 第2版 方法論・実践
オブジェクト指向入門 第2版 方法論・実践
初めてのRuby
初めてのRuby
[24時間365日] サーバ/インフラを支える技術 ~スケーラビリティ、ハイパフォーマンス、省力運用
[24時間365日] サーバ/インフラを支える技術 ~スケーラビリティ、ハイパフォーマンス、省力運用
上流工程UMLモデリング 業務・要求分析からプログラミングへのモデル化技法
上流工程UMLモデリング 業務・要求分析からプログラミングへのモデル化技法
UMLモデリング入門 本質をとらえるシステム思考とモデリング心理学
UMLモデリング入門 本質をとらえるシステム思考とモデリング心理学
Rubyクックブック ―エキスパートのための応用レシピ集
Rubyクックブック ―エキスパートのための応用レシピ集
Code Craft ~エクセレントなコードを書くための実践的技法~
Code Craft ~エクセレントなコードを書くための実践的技法~
ジェネレーティブプログラミング
ジェネレーティブプログラミング
インターフェイス指向設計 アジャイル手法によるオブジェクト指向設計の実践
インターフェイス指向設計 アジャイル手法によるオブジェクト指向設計の実践
販売管理システムで学ぶモデリング講座
販売管理システムで学ぶモデリング講座
Railsレシピブック 183の技
Railsレシピブック 183の技
ビューティフルコード
ビューティフルコード
UMLモデリング入門 本質をとらえるシステム思考とモデリング心理学
UMLモデリング入門 本質をとらえるシステム思考とモデリング心理学
Search


powered bypoweredby