昨年はパスしたのですが、今年は参加することにしました。以下のエントリを投稿しましたので、ご覧いただければ幸いです。
C#のエラーハンドリング実装あれこれ – Qiita
https://qiita.com/masaru_b_cl/items/104f1df602ac56487806
昨年はパスしたのですが、今年は参加することにしました。以下のエントリを投稿しましたので、ご覧いただければ幸いです。
C#のエラーハンドリング実装あれこれ – Qiita
https://qiita.com/masaru_b_cl/items/104f1df602ac56487806
アプリを作ろう! Visual C#入門 Visual C# 2017対応
https://www.amazon.co.jp/dp/4822253554/
サポートページ
http://ec.nikkeibp.co.jp/item/books/P53550.html
本書は無償で入手できるVisual Studio Community 2017でVisual C# 2017を使用して、プログラミングの楽しさが体験できる入門書です。
開発経験がなくても、全9章を順番に学習することで、プログラミングや開発環境の基礎知識、画面のデザイン、コードの書き方、エラーが起きたときの対処方法などを理解できます。
章ごとに短いトピックで区切られているので、自分のペースで学べます。PCスクールなどの教材としても適しています。
という本を執筆し、この度出版する運びとなりました。
企画自体は1年半近く前からありまして、最初はVS2015向けに書く予定でした。しかし、VS2017のBetaが出てき手タイミング変更を余儀なくされ、年末から再スタートしました。
本書の特徴は何といってもまさに「入門」のための書籍であるということです。
例えば、敢えてC#の言語機能を詳しく説明することはせず、サンプルアプリで使う機能に絞って都度説明するようにしています。初学者のには一度に覚えることはなるべく少なくしてあげないと、混乱してしまうためです。
また、本書の構成で一番気を使ったのが、「どの章でやめても、動かして遊べる」ことでした。初心者向けの教育で何より大事なのは、小さなステップで成功体験を積ませることだと私は思っています。したがって、「今やっている章と次の章をやらないと、そもそもプログラムが動かない」というのは、絶対に避けなければいけません。
そういったことを念頭に置き練った構成が、「各章ごとにアプリがパワーアップしていく」というものです。これにより、初心者でも飽きずに、少しずつ動かして動作を確認しながら進めることもできますし、アプリは少しずつ作って成長させていくものだということも教えられるのではないかと考えています。
すでに述べた通り、本書はC#の機能を網羅することを目的とはしていません。言い換えれば、本書はあくまで「1冊目」としての位置付けだということです。プログラミングは膨大な知識とスキルが必要であり、これは1冊の本を読んだだけでは身につくことはまずないでしょう。むしろ1冊で収めようとすることで情報過多になり、かえってわかりにくくなることだってあると思います。そこで、本書でなんとなく「プログラミングというもの」やIDEの使い方を身に着け、次に網羅系のテキストでより深堀して学ぶというのがお勧めです。
身近にWindowsを使っていてプログラミングを始めたいという人がいるようであれば、ぜひ本書を手に取ってみてください。
このエントリは2016/5/7に開催したNiigata.NET 2.0で行ったセッションを再構成したものです。
この 作品 は クリエイティブ・コモンズ 表示 – 継承 4.0 国際 ライセンスの下に提供されています。
テスト駆動開発とは、Kent Beck(ケント・ベック)が提唱した開発手法です。彼の著書であり、「原典」とも呼ばれる「テスト駆動開発入門」(http://www.amazon.co.jp/dp/4894717115)(残念ながら絶版。再販望む!)にはこのように書いてあります。
「動作するきれいなコード」、ロン・ジェフリーズのこの簡潔な言葉は、TDD(テスト駆動開発)の目標である。動作するきれいなコードは、あらゆる理由で価値がある。
それでは、どのように「動作するきれいなコード」を目指していくのでしょう?
まず、「動作するきれいなコード」にたどり着くには2つの道があります。一つはきれいなコードを書きながら動作するように直していくもの。もう一つは動作するコードを書きながらきれいに直していくものです。
(図は日本におけるTDDの第一人者である「和田 卓人(@t_wada)」さんのスライド「TDDのこころ」より拝借しています。)
TDDはこのうち後者の「着実に一歩ずつ進む道」をとります。つまりまず動くようにしてからきれいにしていきます。なぜこの道かというと、何はともあれ「動作」していなければ、製品としての価値はないということが一つです。
他に、この「動かない」→「動く」の線を超えるときに多くの「気づき」が得られるということもあります。フィードバックは早ければ早いほど修正が容易ですので、まずは動くように書き、フィードバックを得て素早く修正していくのがTDDということになります。
TDDはこの着実な道を次のようなサイクルで進んでいきます。
そしてこの手順を小さく回すことで、素早いフィードバックを得ることができます。
これをTDDの界隈では「黄金の回転」と呼んでいます。これはくるくると回りながらどんどんと高みを目指していくイメージから、荒木飛呂彦先生の「スティール・ボール・ラン」に出てくる「黄金の回転」が連想されるため、このように呼ぶようになったそうです。
(スティールボールラン、荒木飛呂彦著より)
では、「動作するきれいなコード」とは何でしょう?それは原典の該当箇所の原文を見ればわかります。
「動作するきれいなコード」は「Clean code that works」であり「Beautiful」ではないのです。つまりは整頓されたコードがきれいなコードであるということになります。
とはいえ、もう少し具体的な指針が欲しいところです。そこで、「きれい」なコードとは何かを知ることのできる書籍をいくつか紹介します。
まずは「リーダブルコード」です。サンプルコードはC#など.NET言語ではありませんが、「リーダブル≒読みやすい」コードを書くためのテクニックに「名前」を付けて完結にまとめているため、最初に読む1冊としては最適です。
次に「リファクタリング」です。コードの挙動を変えずに構成を直す「リファクタリング」のテクニックについて網羅的にまとまっています。「きれいなコード」にどのように直せばよいのかを学ぶのによいでしょう。なお、この本もTDD入門と同じく一度絶版になりましたが、オーム社により再販されています。
これまでの2冊は実際のコードに近い話でしたが、「きれいな」コードは「きれいな」設計から生まれます。そのため、オブジェクト指向プログラミング言語を使ったアプリケーションの「設計」に関する原則を学ぶことも重要です。それを学ぶための書籍を2つ紹介します。
まずは「C#実践開発手法」です。これはその名の通り、C#を使っていわゆる「SOLID原則」と呼ばれるような設計原則を、実際にどのように使えばよいのか実例とともに紹介しています。こういった分野の書籍には「アジャイルソフトウェア開発の奥義」がありましたが、コードがJava(もしくはC++、Smalltalk)だったので、C#でコードが読めるというだけでも本書はありがたいです。もちろん、内容もより新しいものになっています。
そして、オブジェクト指向プログラミング言語の設計原則について学ぶなら「Head First デザインパターン」がおすすめです。本書のタイトルは「デザインパターン」となっていますが、実際はSOLID原則やハリウッド原則といった基本原則を学ぶのに最適な本だと私は思っています。サンプルコードはJavaですが、C#を知っていればそれほど苦も無く翻訳して読めるでしょう。
また、きれいなコードは既存のライブラリ等との統一感も重要です。特に.NET Frameworkは標準ライブラリが巨大なので、勝手なルールでライブラリを作ってしまうと、利用者が混乱してしまいます。
そこで、標準ライブラリの設計ガイドラインを知るために、「.NETのクラスライブラリ設計」も強くお勧めします。この本は単にライブラリ設計のガイドラインが書いてあるだけでなく、過去の失敗についての.NETの中の人の議論が掲載されていることもおすすめポイントの一つです。
さて、こういった書籍などを参考にしつつも「きれいなコード」を目指すことがTDDの目標であることはすでに述べたとおりです。では、目標のさらに先にある「TDDの目的」はなんでしょうか。それは、一言でいうと「健康」です。
TDDはテストを書きますが、それは大きな目的を実現するための手段でしかありません。TDDで目標とするのは「動作するきれいなコード」は、すなわち「変化に対応」しやすいコードであるともいえます。
変化に対応しやすいコードは、言い換えれば不安が少ないコードだともいえます。ユーザーの要望をかなえるためには、コードを変化させなければいけませんが、「動作するきれいなコード」ならば、安心して変更することができます。
そして、この「不安が少ない」状態を一言で表せば、それが「健康」であるということです。
そして、「健康」なコードは不安が少ないため、開発者自身の「健康」にもつながります。一人一人の開発者が健康なら、チーム全体も「健康」であり、ひいては開発、運用している製品、サービスとその開発者、ユーザーなど関係者全員の健康にもつながります。
この大きな目的のために、まずは目標として「動作するきれいなコード」を維持するよう、TDDを行っていきます。
TDDがどういうものかわかりましたが、.NET開発においてTDDをどのように進めていけばよいのでしょうか?次の4つの項目に焦点を当て、順に説明していきます。
まずは、.NETで使える主なテスティングフレームワークをいくつか紹介します。
まず最初は「MSTest」です。MSTestはVisual Studioに標準搭載されたテスティングフレームワークで、現在はExpress Editionでも使用できるため、お手軽さはNo.1です。
次は「NUnit」です。JUnitをはじめとした、いわゆるxUnit系列の正統派オープンソーステスティングフレームワークで、MSTestがVSの下位エディションに搭載されるまでは、デファクトスタンダードでした。
最後は「xUnit.net」です。上記2つより新しく、テストコードの書き方等を一から見直したテスティングフレームワークで、MSのASP.NETチームなどでは、最近のオープンソースプロダクトのテストに使用されています。
ではどのテスティングワークを選べばよいのでしょうか?個人的にはMSTestかNUnitをお勧めします。
MSTestは何といってもVSを入れれば入っているというお手軽さが一番で、必要最小限の機能はあるので、入門には最適だといえるでしょう。また、NUnitは歴史があることもあり、Web上の日本語情報も多く、MSTestよりは高機能(パラメタライズド・テストなど)です。
次にサポートツールについて見ていきましょう。まずは「テストアダプター」です。
MSTestは標準でVS上からテストを実行することができますが、その他のテスティングフレームワークは、専用のテストランナーを使う必要がありました。それを改善し、VSからテストを実行できるようにするのが、「テストアダプター」です。
テストアダプターを導入すれば、NUnitやxUnit.netのテストコードも、VS上から実行して結果を確認できるようになります。
また、テストコードの書き味を改善するため、「Chaining Assertion」というライブラリもお勧めです。
Chaining Assertionはテストコードの書き方を、A.Is(B)の形式で書けるようにします。MSTestの他、NUnitやxUnit.netにも対応しています。後で実例を出すときに、Chaining Assertionを使ったコード例を使って説明します。
次にTDDを行うソリューションの構成を見ていきましょう。.NETでTDDする場合、テスト対象プロジェクトとテストプロジェクトの複数プロジェクト構成にするのが一般的です。
まずテスト対象プロジェクトは*.exe形式の実行ファイルや*.dllのクラスライブラリどちらでも大丈夫です。ただ、クラスライブラリにする方が一般的ではあります。
そして、テストプロジェクトが別になるため、テスト対象の型は原則publicとします。internalな型についても、テスト対象プロジェクトにアセンブリ属性InternalsVisibleToを使い、テストプロジェクトに対してのみ公開することもできます。
次にテストプロジェクトですが、MSTestを使う場合は「単体テストプロジェクト」を選択します。他のOSSテスティングフレームワークを使う場合は、クラスライブラリプロジェクトを作成した後、使用するテスティングフレームワークへの参照を追加します。
次にテストコードの構成を見てみましょう。今回はMSTest+前述のChaining Assertionを使ったテストコードについて説明します。
using Microsoft.VisualStudio.TestTools.UnitTesting; | |
namespace Test.MSTest | |
{ | |
[TestClass] | |
public class UnitTest1 //テストクラス | |
{ | |
[TestInitialize] | |
public void SetUp() { /*テスト初期処理*/ } | |
[TestCleanup] | |
public void TearDown() { /*テスト後処理*/ } | |
[TestMethod] | |
public void TestSomething() | |
{ | |
//テスト主処理 | |
“実際の値”.Is(“期待する値"); | |
} | |
} | |
} |
まず、MSTestのテストクラスであることを示すため、TestClass属性をクラスに付けます。テストクラスはpublic出なければならないことに注意してください。
次に、実際のテストを行うメソッドにはTestMethod属性を付けます。テストメソッドもpublicとし、戻り値はvoid、引数はなしとします。テストメソッドは必要な数だけ複数個作ってかまいません。テストメソッド内では、実際の値と期待値が等しいことを、Isメソッドを使い検証します。
この他、それぞれのテストメソッドが実行される前に毎回実行したい処理、例えばテスト対象オブジェクトの初期化などは、TestInitialize属性のついた、いわゆるSetUpメソッドに書きます。同様に、テストメソッド実行後に毎回実行したい後処理は、TestCleanup属性のついた、いわゆるTearDownメソッドに書きます。
こうして作成したテストコードは、Visual Studio上から実行すると、「テストエクスプローラー」にテストメソッドごとに成功、失敗、そして失敗ならその失敗した箇所の情報が表示されます。
このようにして、VS上でテストを行っていきます。
それでは、実際にTDDを行っている様子の実例を見せましょう。今回はFizzBuzzを題材として使用します。作成したコードは、GitHubにアップしているので、併せて参照してみてください。
まず最初に行うのは「設計」です。作成するプログラムの仕様を元に、ざっと設計して必要と思われるタスクをToDoリストとして作成します。最初にこの工程を行うことで、事前の目標付けとともに、仕様であいまいな部分やどのようにテストを行うのかといった不安要素をなるべくなくします。
ToDoリストを作成したら、その中から一つ選び最初の「失敗する」テストを作成します。今回は「正の整数の場合、その数値を文字列で返す」を選び、このことをテストするために「引数が1の場合、”1″を返す」ことを確認するテストコードを書きます。
この時、自分が「最初のユーザー」となり、使いやすいAPI設計を考えることが重要です。それには、型名やメソッド名、引数の名前や型、戻り値の型など、様々な観点があります。
今回はまずテスト対象プロジェクトFizzBuzzとテストプロジェクトFizzBuzz.Testを作成し、FizzBuzz.TestプロジェクトにFizzBuzzTest.csを追加してFizzBuzzTestクラスを作成しました。なお、テスティングフレームワークにはMSTestを使い、Chaining AssertionをNuGet経由でインストールしています。
PM> Install-Package ChainingAssertion
using System; | |
using Microsoft.VisualStudio.TestTools.UnitTesting; | |
namespace FizzBuzz.Test | |
{ | |
[TestClass] | |
public class FizzBuzzTest | |
{ | |
[TestMethod] | |
public void 引数が1の場合文字列で1を返す() | |
{ | |
// arrange | |
var fizzBuzzer = new FizzBuzzer(); | |
// act | |
string result = fizzBuzzer.Say(number: 1); | |
// assert | |
result.Is("1"); | |
} | |
} | |
} |
(https://github.com/masaru-b-cl/ngtnet2-fizzbuzz/blob/af2891d63f95b2ea60c88e643b335d0ca14159bd/FizzBuzz.Test/FizzBuzzTest.cs)
.NET開発で最初のテストコードを書く時のポイントは以下の通りです。
こうしておくことで、次のフェーズを行う際、VSの機能を使ってコードを自動生成できます。
またこの他、テストコードは適宜日本語で書くことで、その意図がわかりやすくなることもあります。特にC#はメソッド名に使える文字が限られているため、日本語の表現力に頼るのは理にかなっています。
テストコードを作成したら、テストを実行して失敗することを確認します。ここではテストの役割のうち「正しく失敗する」ことを確認することが重要です。
なお、「最初の失敗するテスト」の失敗にはコンパイルエラーも含めてよいです。この段階ではFizzBuzzerクラスは無いのでコンパイルエラーになるはずです。
テストが正常に失敗することを確認したら、いよいよテスト対象コードの実装に入ります。まず最初に行うのは、「仮実装(Fake It)」です。仮実装とは「テストが成功する最速の実装」のことで、リテラル定数を使ってテストが成功するように書いてしまいます。最初に作成したテストコードであれば、戻り値として”1″を返せば、テストが成功するはずです。
一見意味がないようにも見えますが、この手順には失敗しようのない状態で「テストが正しく成功する」ことで、テストの妥当性を担保するという目的があります。もし仮実装でもテストが失敗するようなら、その後本当の実装にしてもテストが成功するわけはないのですから。
VSで仮実装を行う際、まずはテストコードからテスト対象クラスを生成します。コンパイルエラーとなっているFizzBuzzerクラスの箇所でCtrl+.キーを押し、「新しい型の生成…」を選択します。
そして、表示された「型の生成」ダイアログにて、「種類」にclass、「場所」にFizzBuzzプロジェクト、そして「ファイル名」に”FizzBuzzer.cs”を指定して「OK」ボタンをクリックします。
すると指定した通り、FizzBuzzerクラスがFizzBuzzプロジェクトのFizzBuzzer.csファイルに作成されます。
namespace FizzBuzz | |
{ | |
public class FizzBuzzer | |
{ | |
public FizzBuzzer() | |
{ | |
} | |
} | |
} |
次に、FizzBuzzTest.csに戻り、コンパイルエラーとなっているSayメソッドの生成を行います。Ctrl+.を押し、「メソッド ‘FizzBuzzer.Say’ を生成します」を選択します。
すると、未実装のメソッドSayが生成されます。
using System; | |
namespace FizzBuzz | |
{ | |
public class FizzBuzzer | |
{ | |
public FizzBuzzer() | |
{ | |
} | |
public string Say(int number) | |
{ | |
throw new NotImplementedException(); | |
} | |
} | |
} |
テストクラスで引数名、型、戻り値の型を明示していたことで、それに合わせたシグネチャでメソッドが生成されます。
最後に、Sayメソッドが1を返すように仮実装すれば終了です。
using System; | |
namespace FizzBuzz | |
{ | |
public class FizzBuzzer | |
{ | |
public FizzBuzzer() | |
{ | |
} | |
public string Say(int number) | |
{ | |
return "1"; | |
} | |
} | |
} |
この時、FizzBuzzTest.csファイルのSayメソッド呼び出しか所でAlt+F12を押すことで、「定義をここに表示」すると便利です。
テストを実行し、成功することを確認します。Ctrl+R, Aキーを押すことで、すべてのテストを実行できます。
無事テストが成功しました。
テストが成功したので、今度はリファクタリングです。テストが成功する状態を保ったまま、コードを整理します。言い換えれば、挙動を変えずに構造を改善するということです。
なお、テストコードを整えるのもリファクタリングの一つです。ここでは、テストコード内で明示した型や引数名を取り除いてみましょう。
using System; | |
using Microsoft.VisualStudio.TestTools.UnitTesting; | |
namespace FizzBuzz.Test | |
{ | |
[TestClass] | |
public class FizzBuzzTest | |
{ | |
[TestMethod] | |
public void 引数が1の場合文字列で1を返す() | |
{ | |
var fizzBuzzer = new FizzBuzzer(); | |
fizzBuzzer.Say(1).Is("1"); | |
} | |
} | |
} |
(https://github.com/masaru-b-cl/ngtnet2-fizzbuzz/blob/42483eb7eaa217d80440a026572ffd211bb3ddab/FizzBuzz.Test/FizzBuzzTest.cs)
コードを変更したら、テストを実行して成功することを確認しておきましょう。
先に進むため、「小さな一歩」となるような新たなテストを追加します。この時、先ほど仮実装したメソッドにて、同じ仕様を満たすもう一つのテストコードを追加することで、実装1点に対してテスト2点の「三角」で進めることを「三角測量」と呼びます。今回の例でいえば、「正の整数の場合、その整数を文字列にして返す」という仕様に対して、最初に作った1を”1″にして返すテストに加え、2を”2″にして返すかどうかを確認するテストを追加することで、三角測量を行います。
using System; | |
using Microsoft.VisualStudio.TestTools.UnitTesting; | |
namespace FizzBuzz.Test | |
{ | |
[TestClass] | |
public class FizzBuzzTest | |
{ | |
[TestMethod] | |
public void 引数が1の場合文字列で1を返す() | |
{ | |
var fizzBuzzer = new FizzBuzzer(); | |
fizzBuzzer.Say(1).Is("1"); | |
} | |
[TestMethod] | |
public void 引数が2の場合文字列で2を返す() | |
{ | |
var fizzBuzzer = new FizzBuzzer(); | |
fizzBuzzer.Say(2).Is("2"); | |
} | |
} | |
} |
(https://github.com/masaru-b-cl/ngtnet2-fizzbuzz/blob/f21f34237754464babe6e6746a7f52df94295f4b/FizzBuzz.Test/FizzBuzzTest.cs)
テストを追加したらすべてのテストを再実行し、追加したテストだけが失敗することを確認します。もしこの時複数のテストが失敗するようであれば、それは良くない設計であることを示す臭いです。「良くない」とは仕様でなく内部実装に対してテストしている、責務分割が不十分で一つの変更があちこちに影響するような作りになっている可能性が高いのです。
無事2つ目のテストだけが失敗しました。
あとは、失敗した2つ目のテストについて、テストが成功するまで実装を行います。このとき、仮実装で使用した定数、リテラルを、引数や変数を使うように直します。サンプルでは、Sayメソッドの引数numberを文字列にして返すようにします。
using System; | |
namespace FizzBuzz | |
{ | |
public class FizzBuzzer | |
{ | |
public FizzBuzzer() | |
{ | |
} | |
public string Say(int number) | |
{ | |
return $"{number}"; | |
} | |
} | |
} |
(https://github.com/masaru-b-cl/ngtnet2-fizzbuzz/blob/f21f34237754464babe6e6746a7f52df94295f4b/FizzBuzz/FizzBuzzer.cs)
修正後にテストを実行し、2つのテスト両方が成功することを確認します。
2つのテストで共通で使っているfizzBuzz変数をフィールドにしてSetUpメソッドで初期化したり、整数が整数の文字列を返すテストは一つあれば十分なので、一方を消したりします。
using System; | |
using Microsoft.VisualStudio.TestTools.UnitTesting; | |
namespace FizzBuzz.Test | |
{ | |
[TestClass] | |
public class FizzBuzzTest | |
{ | |
private FizzBuzzer fizzBuzzer; | |
[TestInitialize] | |
public void SetUp() | |
{ | |
fizzBuzzer = new FizzBuzzer(); | |
} | |
[TestMethod] | |
public void 引数が正の数の場合文字列でその数を返す() | |
{ | |
fizzBuzzer.Say(1).Is("1"); | |
} | |
} | |
} |
(https://github.com/masaru-b-cl/ngtnet2-fizzbuzz/blob/f3ffd123918d51a20a2e6cb8b4892097a9adccbe/FizzBuzz.Test/FizzBuzzTest.cs)
テストを実行し、成功することを確認します。
ここまでで事前に作成したToDoリストの最初の仕様の実装が終わったので、完了したタスクを消してしまいます。
今度は別の仕様について実装していこうと思うので、次のテストを作成し、追加します。3の倍数のテストを追加してみます。
using System; | |
using Microsoft.VisualStudio.TestTools.UnitTesting; | |
namespace FizzBuzz.Test | |
{ | |
[TestClass] | |
public class FizzBuzzTest | |
{ | |
private FizzBuzzer fizzBuzzer; | |
[TestInitialize] | |
public void SetUp() | |
{ | |
fizzBuzzer = new FizzBuzzer(); | |
} | |
[TestMethod] | |
public void 引数が正の数の場合文字列でその数を返す() | |
{ | |
fizzBuzzer.Say(1).Is("1"); | |
} | |
[TestMethod] | |
public void 引数が3の倍数の場合Fizzを返す() | |
{ | |
fizzBuzzer.Say(3).Is("Fizz"); | |
} | |
} | |
} |
(https://github.com/masaru-b-cl/ngtnet2-fizzbuzz/blob/f4b7c5a74d3ccada1787a583b96ab66d613705e4/FizzBuzz.Test/FizzBuzzTest.cs)
実装が自明である場合、仮実装をスキップしてしまっても構いません。これを「明白な実装」と呼び、イメージとしては開発スピードアップのため、ギアをあげるイメージです。今回は引数を3で割った余が0の時に”Fizz”を返すよう実装します。
using System; | |
namespace FizzBuzz | |
{ | |
public class FizzBuzzer | |
{ | |
public FizzBuzzer() | |
{ | |
} | |
public string Say(int number) | |
{ | |
if (number % 3 == 0) | |
{ | |
return "Fizz"; | |
} | |
return $"{number}"; | |
} | |
} | |
} |
(https://github.com/masaru-b-cl/ngtnet2-fizzbuzz/blob/da6c8c0288634b67789fcfa17965bf4ff761910c/FizzBuzz/FizzBuzzer.cs)
以上のような手順を繰り返して、ToDoリストの項目を全部消したら、実装が完了したということになります。
最後に、TDDを行う上でのいくつかの指針を紹介します。
まず、「いつ」TDDを行えばよいのかというと、それは「不安」を感じたとき、ということになります。例えば、ライブラリの使い方に自信がなかったり、第三者の実装が信用できない時など、開発に関連する不安があれば、それをテストで保護してあげるのです。
ただし、今作成しているのが「使い捨て」のコードであれば、TDDを行わない方が良いこともあります。テストコードを作成しても、そのテストが数回しか実行されないようであれば、テストはただのコストでしかありません。
その他、GUIのコードについてはTDDを行いません。GUIの自動化テストを作成するには非常に手間がかかり高コストな作業です。しかし、GUIは一番ユーザーに近いところであることから、変更が頻繁に起こります。そうすると、せっかく作ったテストもすぐに変更しないといけなくなってしまいます。
また、トライ&エラーであれこれ試すような時もTDDは向いていません。ただし、あれこれ試した結果、確定した実装に対してテストを書いても問題ありません。「テストファースト」にはこだわらなくても大丈夫です。
一般的に「良い」とされるテストの特徴に「F.I.R.S.T」と呼ばれるものがあります。
これら5つの特徴には優先度があり、特に重要なのはIとR、続いてF、S、Tです。少なくともIとRだけは考慮したテストを書きましょう。
ToDoリストを元にどのようなテストを行えばよいのかについては、各種のテスト技法を参考としましょう。基本的なテスト技法として、同値クラス、境界値分析くらいは考慮したテストを書きましょう。
「テストがないコードはレガシーコードだ!」のキャッチコピーでおなじみの「レガシーコード改善ガイド」を参考に、小さく初めて少しずつ範囲を広げていきましょう。
最後に大事なことですが、TDDはあくまで「センス」ではなく「スキル」です。訓練次第で誰でも習得画家のなものなのです。たまに「テストが書けるようになったらTDDをやってみよう」みたいなことを聞くことがありますが、「できるようになったら始める」というのは永遠に始めることはできません。まずは始めてみて、うまくいかなければ直せばよいのですから。
また、本来はチームを巻き込んだ形TDDができるのが理想ですが、まずはあなた一人でも始めることができます。やってみて、良いものであれば他の人に勧めて、徐々にTDD人口を増やしていきましょう。
そして、TDDによって安心を得て、「健康になりましょう」
この記事はC# Advent Calendar 2015の12日目の記事です。
using System; | |
using Microsoft.VisualStudio.TestTools.UnitTesting; | |
namespace StringInterpolation | |
{ | |
[TestClass] | |
public class StringInterpolationTest | |
{ | |
[TestMethod] | |
public void 文字列の埋め込み() | |
{ | |
// $"…"で括った文字列が対象 | |
// 埋め込む箇所に{}で括って対象の式を指定する | |
var name = "Sho"; | |
Assert.AreEqual("Hello Sho!", $"Hello {name}!"); | |
} | |
[TestMethod] | |
public void 文字列の空白埋め() | |
{ | |
// {式,桁数}のように,に続けて桁数を指定する | |
var name = "Sho"; | |
Assert.AreEqual(" Sho is first name", $"{name,10} is first name"); | |
} | |
[TestMethod] | |
public void 暗黙型変換() | |
{ | |
// 文字列以外はToStringの結果が設定される | |
var age = 35; | |
Assert.AreEqual("age is 35", $"age is {age}"); | |
// なので、DateTime型などはそのままだと時刻まで入ってしまう | |
var birthday = new DateTime(1980, 10, 24); | |
Assert.AreEqual("Birthday is 1980/10/24 0:00:00", $"Birthday is {birthday}"); | |
} | |
[TestMethod] | |
public void コロンを含む式の利用() | |
{ | |
// 対象の式を()で括る | |
var age = 35; | |
Assert.AreEqual("you are middle age", | |
$"you are {(age <= 20 ? "young" : age > 45 ? "senior" : "middle age")}"); | |
// 改行は含められない | |
// Assert.AreEqual("you are middle age", | |
// $"you are {(age < 30 ? "young" : | |
// age > 45 ? "senior" : | |
// "middle age")}"); | |
} | |
[TestMethod] | |
public void 中括弧を含む式の利用() | |
{ | |
// {{, }}のように二重にする | |
var name = "Sho"; | |
Assert.AreEqual("{Sho}", $"{{{name}}}"); | |
} | |
[TestMethod] | |
public void 日付のフォーマット() | |
{ | |
// :(コロン)に続けて日付の編集書式を指定する | |
var birthday = new DateTime(1980, 10, 24); | |
Assert.AreEqual("Birthday is 1980-10-24", $"Birthday is {birthday:yyyy-MM-dd}"); | |
} | |
[TestMethod] | |
public void 数値のフォーマット() | |
{ | |
// :(コロン)に続けて数値の編集書式を指定する | |
// 1) 小数点以下桁数指定 | |
var height = 173m; | |
Assert.AreEqual("Height is 173.00(cm)", $"Height is {height:0.00}(cm)"); | |
// 2) カンマ編集 | |
var wantsSalary = 300000m; | |
Assert.AreEqual("wants 300,000", $"wants {wantsSalary:#,#}"); | |
// 3) 数字の桁指定 | |
var serialNo = 123; | |
Assert.AreEqual("my serial is 000123", $"my serial is {serialNo:d6}"); | |
// 4) 動的桁指定 | |
var digits = 10; | |
// !! 書式指定文字列の中で文字列補完は使えない !! | |
// Assert.AreEqual("long serial 0000000123", $"long serial {serialNo:d{digits}}"); | |
// a) String.Formatと組み合わせるか | |
Assert.AreEqual("long serial 0000000123", | |
String.Format($"long serial {$"{{0:d{digits}}}"}", serialNo)); | |
// b) {}の中でToStringする | |
Assert.AreEqual("long serial 0000000123", | |
$"long serial {serialNo.ToString($"d{digits}")}"); | |
// 【緩募】もっと良い動的な書式指定文字列の指定方法 | |
} | |
[TestMethod] | |
public void オブジェクトのメンバーの利用() | |
{ | |
// .でオブジェクトのメンバーにもアクセスできる | |
var person = new { Name = "Sho", Age = 35, Married = true }; | |
Assert.AreEqual("name:Sho, age:35 (既婚)", | |
$"name:{person.Name}, age:{person.Age} ({(person.Married ? "既婚" : "未婚")})"); | |
} | |
} | |
} |
Enjoy!
監訳者の長沢智治氏(@tnagasawa)より献本御礼。
C#実践開発手法 – デザインパターンとSOLID原則によるアジャイルなコーディング
本書はAdaptive Code via C#: Agile coding with design patterns and SOLID principles(Microsoft Press, 2014)の日本語版で、変化に容易に適応できるソフトウェア開発を実現するために、
アジャイル開発、デザインパターン、SOLID原則を、C#でどのように実践するかを解説する書籍です。方法論と実践の間の橋渡しをする解説書で、サンプルコードはVisual Studio 2013ベース。
C#の基本をひととおり理解した開発者が、ワンランク上を目指すために読んでおきたい1冊です。
SOLID原則やデザインパターンに関して説明する書籍として著名なのはボブおじさん(Uncle Bob)ことロバート・C・マーチンの「アジャイルソフトウェア開発の奥義」です。しかし、サンプルコードはJavaであり、C#を題材にした同種の本は、これまではありませんでした。本書はそういった現状を解決してくれる書籍になるのではと、期待に胸を躍らせていました。
そして、本書を読んでみて、率直な感想としては「これはいいものだ」ということです。オブジェクト指向プログラミングのSOLID原則を平易なコードを使って解説してあり、初学者でもある程度の雰囲気はつかめるでしょう。第3部ではASP.NET MVCのWebアプリケーションを用いて、実践的なSOLID原則を考慮したリファクタリングの過程も紹介してあり、一つの指針として参考になるでしょう。
また、本書の他にない特徴は、アジャイルプロセスの一つである「スクラム」の簡易な解説書でもあるということです。第1部でプロセスや用語について解説し、第3部ではその用語も使いつつ、スクラムの「2スプリント」分の進め方を疑似体験できるようになっています。このあたりは「アジャイルソフトウェア開発の奥義」が同じくアジャイルプロセスのXP(エクストリーム・プログラミング)を最初に紹介し、同じように実践例を見せていることを強く意識しているように感じました。
あと、地味だけどよいなぁと思ったのは、第2章という早めの段階で「依存関係」について、丁寧に説明していることです。アセンブリの依存関係、型の依存関係、メソッドの依存関係の解説ももちろんですが、実行時のアセンブリ探索の手順まで簡易に説明してあるのが良いです。このあたり、初心者が2つ以上のアセンブリで構成されるアプリケーションを作成するときに結構「ハマり」ポイントですので、
ただ、情報量という面では「アジャイルソフトウェア開発の奥義」にはかないません。パッケージ(アセンブリ)の構成やコードメトリクスのような、さらに進んだ話題は相変わらずこちらを参照することになるでしょう。また、「C#ならでは」というものも、それほど多くはないという印象でした。この点については少し残念でした。
そういった意味で、本当に初学者がSOLID原則を学ぶ書籍としては、個人的にはいまだに「Head First デザインパターン」を勧めたいです。コードはJavaですが、ほぼC#でも再現可能であり、いわゆる「GoFデザインパターン」の主要なものを題材に、順を追ってSOLID原則をはじめとしたオブジェクト指向プログラミングで大事な「原則」の多くを学べるからです。
とはいえ、ほぼ最新に近いC#のコードでこういった各種の原則の一部を学べる意義は大きいと思います。現場ですぐに使える知識を本書で学んでから、より高度な内容を学んでいくというのも良いでしょう。まさに「C#の基本をひととおり理解した開発者が、ワンランク上を目指すために読んでおきたい1冊」として、ふさわしい一冊です。
最近ロギングライブラリでNLogを使ってるんですが、ログの各項目をタブ区切りにすると、Excelに貼り付けるのが楽だなぁと思い、その方法を調べました。
<?xml version="1.0" encoding="utf-8"?> | |
<configuration> | |
<configSections> | |
<section name="nlog" type="NLog.Config.ConfigSectionHandler, NLog"/> | |
</configSections> | |
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd" | |
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> | |
<targets> | |
<target name="file" xsi:type="File" fileName="${basedir}/log.txt" encoding="UTF-8"> | |
<!– タブ区切りのレイアウト –> | |
<layout xsi:type="CSVLayout" delimiter="Tab"> | |
<!– ヘッダーは出さない –> | |
<withHeader>false</withHeader> | |
<!– 項目はクォートしない –> | |
<quoting>Nothing</quoting> | |
<column layout="${longdate}" /> | |
<column layout="${level}" /> | |
<column layout="${message}" /> | |
</layout> | |
</target> | |
</targets> | |
<rules> | |
<logger name="*" minlevel="Debug" writeTo="file" /> | |
</rules> | |
</nlog> | |
</configuration> |
ポイントは次の通り。
using System; | |
namespace ConsoleApplication1 | |
{ | |
class Program | |
{ | |
static void Main(string[] args) | |
{ | |
var logger = NLog.LogManager.GetCurrentClassLogger(); | |
var text = "改行や" + Environment.NewLine + "タブ文字\tを含む項目"; | |
// 独自項目はタブ文字でJoinする | |
// 改行やタブ文字を含む項目は、自前でクォートする | |
var message = String.Join("\t", new[]{"field1", "field2", "\"" + text + "\""}); | |
logger.Info(message); | |
} | |
} | |
} |
ポイントは次の通り。
以上で準備が整いました。あとは実行すると、次のようなログが出力されます。
2014-08-06 01:52:13.8583 Info field1 field2 "改行や | |
タブ文字 を含む項目" |
ネタ元 : Announcing the .NET Framework 4.5.2 – .NET Framework Blog
というような、WinFormsの高DPI対応が含まれています。
High DPI Improvements is an opt-in feature to enable resizing according to the system DPI settings for several glyphs or icons for the following Windows Forms controls: DataGridView, ComboBox,
ToolStripComboBox, ToolStripMenuItem and Cursor. Here are examples of before and after views once this change is opted into.
他の内容については、ネタ元をご覧ください。
このエントリは「C# Advent Calendar 2013 – Adventar」への参加エントリです。
前日は@yone64さんの「WindowsストアアプリでReflection | 泥庭」でした。
もはや空気のようになったLINQですが、いまだに活用されていない現場を見ることもあり、非常に残念に思います。
そこで、本エントリではいわゆる「キーブレイク処理」をLINQで置き換え、性能にほとんど差がないことを示し、LINQ導入の一助としたいと思います。
取引先と商品ごとに単価と数量を持つ売上データを、明細、小計(商品ごと)、大計(取引先ごと)、総計を出すプログラムを考えます。
元データはこんな感じで生成します。
var q = from i in Enumerable.Range(1, 100) | |
from j in Enumerable.Range(1, 100) | |
from k in Enumerable.Range(1, 10) | |
select new Sales | |
{ | |
Customer = string.Format("取引先{0:d2}", i), | |
Product = string.Format("商品{0:d2}", j), | |
UnitPrice = Convert.ToDecimal(j * 100), | |
Quantity = k | |
}; | |
var source = q.ToArray(); |
こんな感じで、オーソドックスなキーブレイク処理を行います。
#久々にキーブレイク処理とか書いたf(^^;
var oldCustomer = ""; | |
var newCustomer = ""; | |
var oldProduct = ""; | |
var newProduct = ""; | |
var totalQuantity = 0L; | |
var totalPrice = 0m; | |
var totalCustomerQuantity = 0L; | |
var totalCustomerPrice = 0m; | |
var totalProductQuantity = 0L; | |
var totalProductPrice = 0m; | |
foreach (var sales in source) | |
{ | |
if (oldCustomer == "") | |
{ | |
oldCustomer = sales.Customer; | |
oldProduct = sales.Product; | |
} | |
newCustomer = sales.Customer; | |
newProduct = sales.Product; | |
if (oldCustomer != newCustomer || oldProduct != newProduct) | |
{ | |
Console.WriteLine("小計 P:{0}, {1}, {2}", oldProduct, totalProductQuantity, totalProductPrice); | |
totalProductQuantity = 0L; | |
totalProductPrice = 0m; | |
oldProduct = newProduct; | |
} | |
if (oldCustomer != newCustomer) | |
{ | |
Console.WriteLine("大計 P:{0}, {1}, {2}", oldCustomer, totalCustomerQuantity, totalCustomerPrice); | |
totalCustomerQuantity = 0L; | |
totalCustomerPrice = 0m; | |
oldCustomer = newCustomer; | |
} | |
totalProductQuantity += sales.Quantity; | |
totalProductPrice += sales.UnitPrice * sales.Quantity; | |
totalCustomerQuantity += sales.Quantity; | |
totalCustomerPrice += sales.UnitPrice * sales.Quantity; | |
totalQuantity += sales.Quantity; | |
totalPrice += sales.UnitPrice * sales.Quantity; | |
Console.WriteLine(sales); | |
} | |
Console.WriteLine("小計 P:{0}, {1}, {2}", oldProduct, totalProductQuantity, totalProductPrice); | |
Console.WriteLine("大計 P:{0}, {1}, {2}", oldCustomer, totalCustomerQuantity, totalCustomerPrice); | |
Console.WriteLine("総計 {0}, {1}", totalQuantity, totalPrice); |
LINQ側はこんな感じ。取引先でグルーピングしたものをぶん回し、内部でさらに商品でグルーピングしています。もっと良い書き方があればぜひ教えていただきたいところ。
var salesOfCustomers = source.GroupBy(x => x.Customer); | |
foreach (var salesOfCustomer in salesOfCustomers) | |
{ | |
var salesOfProducts = salesOfCustomer.GroupBy(x => (x.Product)); | |
foreach (var salesOfProduct in salesOfProducts) | |
{ | |
foreach (var sales in salesOfProduct) | |
{ | |
Console.WriteLine(sales); | |
} | |
Console.WriteLine("小計 P:{0}, {1}, {2}", salesOfProduct.Key, salesOfProduct.Sum(x => x.Quantity), | |
salesOfProduct.Sum(x => x.UnitPrice * x.Quantity)); | |
} | |
Console.WriteLine("大計 C:{0}, {1}, {2}", salesOfCustomer.Key, salesOfCustomer.Sum(x => x.Quantity), | |
salesOfCustomer.Sum(x => x.UnitPrice * x.Quantity)); | |
} | |
Console.WriteLine("総計 {0}, {1}", source.Sum(x => x.Quantity), source.Sum(x => x.UnitPrice * x.Quantity)); |
それぞれ10回ぶん回して、処理時間の平均をとってみました。その結果がこちら(単位msec)。
多少キーブレイクのほうが早い感じになってますが、誤差の範囲といってもいいんじゃないでしょうか。現に何度かやったらLINQの方がごくわずかに早いこともありました。
つまり、今回のケースではLINQを使ってもパフォーマンス上不利になることはないということです。これは、処理自体より、標準出力の方がボトルネックになってるからかなーとも思いますが、キーブレイク処理するようなときは大体帳票出したりするときなので、CPUがいくら頑張ってもI/Oが遅いはずであることを考えると、LINQを使わない理由はないかなと。コード短くて何やってるかすぐ分かりますし。
今回の検証ソースはGistにあげてあります。
using System; | |
using System.Collections.Generic; | |
using System.Diagnostics; | |
using System.Linq; | |
using System.Text; | |
using System.Threading.Tasks; | |
namespace ConsoleApplication1 | |
{ | |
class Program | |
{ | |
class Sales | |
{ | |
public string Customer { get; set; } | |
public string Product { get; set; } | |
public decimal UnitPrice { get; set; } | |
public int Quantity { get; set; } | |
public override string ToString() | |
{ | |
return string.Format("C:{0}, P:{1}, U:{2}, Q:{3}", Customer, Product, UnitPrice, Quantity); | |
} | |
} | |
static void Main(string[] args) | |
{ | |
var q = from i in Enumerable.Range(1, 100) | |
from j in Enumerable.Range(1, 100) | |
from k in Enumerable.Range(1, 10) | |
select new Sales | |
{ | |
Customer = string.Format("取引先{0:d2}", i), | |
Product = string.Format("商品{0:d2}", j), | |
UnitPrice = Convert.ToDecimal(j * 100), | |
Quantity = k | |
}; | |
var source = q.ToArray(); | |
const int times = 10; | |
var linqTime = UsingLinq(times, source); | |
var keyBreakTime = UsingKeyBreak(times, source); | |
Console.WriteLine(); | |
Console.WriteLine("■LINQ:{0}", linqTime); | |
Console.WriteLine("■Key Break:{0}", keyBreakTime); | |
} | |
private static decimal UsingLinq(int times, IEnumerable<Sales> source) | |
{ | |
var sw = new Stopwatch(); | |
var totalTime = 0L; | |
for (var i = 0; i < times; i++) | |
{ | |
sw.Reset(); | |
sw.Start(); | |
var salesOfCustomers = source.GroupBy(x => x.Customer); | |
foreach (var salesOfCustomer in salesOfCustomers) | |
{ | |
var salesOfProducts = salesOfCustomer.GroupBy(x => (x.Product)); | |
foreach (var salesOfProduct in salesOfProducts) | |
{ | |
foreach (var sales in salesOfProduct) | |
{ | |
Console.WriteLine(sales); | |
} | |
Console.WriteLine("小計 P:{0}, {1}, {2}", salesOfProduct.Key, salesOfProduct.Sum(x => x.Quantity), | |
salesOfProduct.Sum(x => x.UnitPrice * x.Quantity)); | |
} | |
Console.WriteLine("大計 C:{0}, {1}, {2}", salesOfCustomer.Key, salesOfCustomer.Sum(x => x.Quantity), | |
salesOfCustomer.Sum(x => x.UnitPrice * x.Quantity)); | |
} | |
Console.WriteLine("総計 {0}, {1}", source.Sum(x => x.Quantity), source.Sum(x => x.UnitPrice * x.Quantity)); | |
sw.Stop(); | |
Console.WriteLine(sw.ElapsedMilliseconds); | |
totalTime += sw.ElapsedMilliseconds; | |
} | |
var averageTime = (decimal)totalTime / times; | |
return averageTime; | |
} | |
private static decimal UsingKeyBreak(int times, IEnumerable<Sales> source) | |
{ | |
var sw = new Stopwatch(); | |
var totalTime = 0L; | |
for (var i = 0; i < times; i++) | |
{ | |
sw.Reset(); | |
sw.Start(); | |
var oldCustomer = ""; | |
var newCustomer = ""; | |
var oldProduct = ""; | |
var newProduct = ""; | |
var totalQuantity = 0L; | |
var totalPrice = 0m; | |
var totalCustomerQuantity = 0L; | |
var totalCustomerPrice = 0m; | |
var totalProductQuantity = 0L; | |
var totalProductPrice = 0m; | |
foreach (var sales in source) | |
{ | |
if (oldCustomer == "") | |
{ | |
oldCustomer = sales.Customer; | |
oldProduct = sales.Product; | |
} | |
newCustomer = sales.Customer; | |
newProduct = sales.Product; | |
if (oldCustomer != newCustomer || oldProduct != newProduct) | |
{ | |
Console.WriteLine("小計 P:{0}, {1}, {2}", oldProduct, totalProductQuantity, totalProductPrice); | |
totalProductQuantity = 0L; | |
totalProductPrice = 0m; | |
oldProduct = newProduct; | |
} | |
if (oldCustomer != newCustomer) | |
{ | |
Console.WriteLine("大計 P:{0}, {1}, {2}", oldCustomer, totalCustomerQuantity, totalCustomerPrice); | |
totalCustomerQuantity = 0L; | |
totalCustomerPrice = 0m; | |
oldCustomer = newCustomer; | |
} | |
totalProductQuantity += sales.Quantity; | |
totalProductPrice += sales.UnitPrice * sales.Quantity; | |
totalCustomerQuantity += sales.Quantity; | |
totalCustomerPrice += sales.UnitPrice * sales.Quantity; | |
totalQuantity += sales.Quantity; | |
totalPrice += sales.UnitPrice * sales.Quantity; | |
Console.WriteLine(sales); | |
} | |
Console.WriteLine("小計 P:{0}, {1}, {2}", oldProduct, totalProductQuantity, totalProductPrice); | |
Console.WriteLine("大計 P:{0}, {1}, {2}", oldCustomer, totalCustomerQuantity, totalCustomerPrice); | |
Console.WriteLine("総計 {0}, {1}", totalQuantity, totalPrice); | |
sw.Stop(); | |
Console.WriteLine(sw.ElapsedMilliseconds); | |
totalTime += sw.ElapsedMilliseconds; | |
} | |
var averageTime = (decimal)totalTime / times; | |
return averageTime; | |
} | |
} | |
} |
次は?
MS MVPの@hiroyuki_moriさんです!
ポイント
- テスティングフレームワークはMSTest
- サポートライブラリにChaining Assertion
- テストケースにはカテゴリーを付け、partialクラスを用いて複数ファイルに分割
- Versionという名前はSystem.Versionクラスで予約済みなので、JdkVersionに変更
- JdkVersionはイミュータブルなValueObjectとして、structとして定義
というわけで、遅ればせながら演習をやってみています。
チャレンジングなこととしては、テストケースにカテゴリを付け、カテゴリ毎にpatialクラスに分割して記述しています。
テスト実行結果はこんな感じ。VS2012 Update 1の追加機能である、カテゴリごとのテスト結果表示を行っています。
分割したファイルをグループ化するには、*.csprojを直接編集して、対象ファイルCompile要素の中に、DependUpon要素として親となるファイルを指定するだけです。
<Compile Include="JdkVersionTest.cs" /> | |
<Compile Include="JdkVersionTest.IsValid.cs"> | |
<DependentUpon>JdkVersionTest.cs</DependentUpon> | |
</Compile> |
VS2012 Pro以上ならば、VSCommands拡張を使って、GUIで簡単にグループ化できます。
VSCommandsおすすめ!
鍵付きアカウントな方から「bool 返す TryHoge を実装してあげるんだ、なるほど。これってfalseが返ると例外発生させるのかな」と聞かれたので、やってみましょうそうしましょう。
こんな適当なDynamicObjectを拵えて実行してみると。
using System; | |
using System.Collections.Generic; | |
using System.Dynamic; | |
using System.Linq; | |
using System.Text; | |
using System.Threading.Tasks; | |
namespace ConsoleApplication1 | |
{ | |
class MyDynamic : DynamicObject | |
{ | |
public override bool TryGetMember(GetMemberBinder binder, out object result) | |
{ | |
result = binder.Name; | |
return false; | |
} | |
} | |
class Program | |
{ | |
static void Main(string[] args) | |
{ | |
dynamic clazz = new MyDynamic(); | |
Console.WriteLine(clazz.hoge); | |
} | |
} | |
} |
予想通り例外発生です。おめでとうございます。