JUnit 実践入門 体系的に学ぶユニットテストの技法 第15章 継続的 テスト すばやいフィードバックを手に入れる

第15章 継続的テスト すばやいフィードバックを手に入れる

  • 開発においては、欠陥の修正による恩恵と、リスクのバランスを示している考慮する必要がある
  • テストを早い段階から繰り返し行うことで、リスクをカバーできる。

15.1 継続的テスト

ユニットテストを開発に導入するためには、効果を体感させることが重要であり、その方法として、継続的テストが有用である。

開発チームへのすばやいフィードバック

ユニットテストの恩恵は、「開発チームへのすばやいフィードバック」である。

早期からテストする

  • ユニットテストは、開発早期段階から実施可能。
  • ユニットテストは、内部実装を考慮したテストであるから、プロダクションコードの実装と同時に作成すると効率がよい。

自動的にテストする

ユニットテストは、実行を自動化できる。

繰り返しテストする

リグレッションの早期検知、修正が可能となるため、リグレッションが発生することを恐れ、コード修正が行えなくなる事態を回避できる。

継続的テストとは

  • 継続的テスト continuous testing とは、早い段階からテストを繰り返し実施するプラクティスのこと。
  • ソフトウェアの品質を直接的には高めないが、開発チームに、ソフトウェアリリースへの自信を与える。

継続的テストを行うための3つの要素

以下を現代ソフトウェア開発の三本柱という。

ユニットテスト

JUnitを利用してテストコードを書くことで、プログラムとして実行可能となる。

自動化

バージョン管理

  • テスト失敗時に差分を確認することで、原因を究明できる。
  • 原因を救命できない際に、ひとまず一つ前の正常バージョンに切り戻すことができる。

継続的テストの運用

  • バージョン管理システムに変更があったタイミングで、ユニットテストを含む自動化されたビルドプロセスが実行されるのが一般的。
  • テスト失敗をゼロにすることが大事なのではなく、発生した失敗を早期に検知・修正することが大事。

継続的テストとリファクタリング

継続的テストがリファクタリングを行うことを心理面で容易にし、結果としてきれいなソースコードをもたらす。

ユニットテストリファクタリング

積極的なリファクタリング

継続的テストが実施されている環境では、開発者は積極的にリファクタリングを行うことができる。

15.2 Maven によるビルドプロセスの自動化

Maven とは

  • Apache Foundation が提供する、Java 用のビルドツール。
    • Maven 以前は Antが主流だった。
    • Maven は Ant に欠けていた、パッケージ管理のシステムが提供されている。

Maven の準備

Eclipse から 管理することも可能であるが、基本的にはコマンドラインから実行するツールであるため、公式サイトからダウンロードしたアーカイブを適当なディレクトリに展開し、パスを通しておく。

Maven プロジェクトと POM

  • MavenEclipse 同様、ソースコードやリソースをプロジェクト単位で管理する。
  • Maven プロジェクトでは、ルートディレクトリに XML ファイルを配置し、プロジェクトの構成を定義する。
    • この定義ファイルのことは POM project object model と呼ばれ、通常 pom.xml というファイル名とする。
  • プロジェクト構成を定義した POM を追加すれば、どんなプロジェクトでも Maven を利用できる。
    • ただし Maven プロジェクトの標準的にディレクトリ構成に合わせておくのがベター。

Maven プロジェクトの作成

  • Eclipse から簡単に作成できる。
    • [新規] -> [プロジェクト] -> [Maven] -> [Maven プロジェクト] -> [次へ]
      • グループId groupId:
        複数のプロダクトをグループ化した識別子。Javaプログラムのパッケージに相当。
      • アーティファクトId artifactId:
        そのプロダクトの識別子(名前)となる。
      • バージョン version:
        そのプロダクトのバージョンを表し、一般的には、「メジャーバージョン.マイナーバージョン.リビジョン」と付与する。

Maven のプロジェクト構成

  • 標準では、プロダクションコードとテストコードを独立したソースフォルダに作成する。リソースファイルも独立したソースファイルに作成するため、4つのソースフォルダが作成される。
  • ビルド時の成果物は target 配下に出力されるため、ソースコード管理システム(Subversion, Git 等)利用時は、target フォルダを除外フォルダとすること。

依存ライブラリの管理

依存ライブラリを追加するには、そのライブラリのグループId、アーティファクトId、バージョンを POM に設定すれば良い。

Mavenリポジトリ

依存ライブラリの追加

ソースコードエンコーディング設定

Mavenプラグイン

プラグインは、大きく4つに大分される。 - コンパイルやテストといったコア機能に関連するもの - JAR や WAR の作成などパッケージングに関連するもの - テスト結果や Javadoc などのレポート作成に関連するもの - それ以外の拡張機能に関連するもの

プラグインの設定

Maven によるビルドの実行とフェーズプラグインの設定

テストをスキップする

Eclipse から実行する

プラグインとゴール

ゴールを実行する

Maven によるカテゴリ化リスト

Maven によるカバーレッジレポート

JaCoCo プラグインの導入

JaCoCo によるカバーレッジレポート

15.3 バージョン管理システムによる継続的テストの運用

バージョン管理システムとは

バージョン管理システムへのコミット

Subversion でのテストサイクル

Git や Mercurial でのテストサイクル

15.4 Jenkins による継続的インテグレーション

継続的インテグレーションとは

Jenkins とは

Jenkins の導入

Jenkins のジョブ

ジョブの作成

ジョブの設定

バージョン管理システムの設定

ビルドゴールのの設定

ジョブの実行

ビルドトリガーの設定

Jenkinsのリモートビルド機能

バージョン管理システムのフック機能

継続的インテグレーションの効果

テストの実行コスト低減

すばやいフィードバック

テスト結果の履歴

JUnit 実践入門 体系的に学ぶユニットテストの技法 第16章 テスト駆動開発 テストファーストで設計する

第16章 テスト駆動開発 テストファーストで設計する

16.1 テスト駆動開発 Test Driven Development とは

  • TDD は、ユニットテストをプロダクションコードよりも先に記述することを原則とした開発手法のこと。
  • TDD は、ゴールを少し先に設定し、それを短期間でクリアすることを繰り返し、最終的なゴールを目指す。
    • 一気に飛躍した結果を求めない。

16.2 テスト駆動開発のサイクル

  1. 設計する
  2. テストコードを書く
    • レッド: テスト失敗
  3. プロダクションコードを書く
    • グリーン: テスト成功
  4. リファクタリングする
    • グリーン: テスト成功

このサイクルを、レッド - グリーン - リファクタリングと呼ぶ。

設計する

  • 何をテストしたらよいかわかるまで、設計する。
  • 主にインターフェイスの観点から設計を行う
    • 仔細に至る部分は、作ってみなければわからない事が多く、はじめから完璧に洗い出すことは難しいため。

テストコードを書く

  • 動作するドキュメントとしてのテストコードを書く。ただし、1サイクルで作成するテストケースは、原則一つ

プロダクションコードを書く

  1. コンパイルエラーの除去
  2. テストコード実行 -> レッド(エラー) (未実装のため)
  3. グリーンになるよう、手を加える
    • 脇目をふらず、ただグリーンにすることを目的に!
    • 例外処理等はここでは実装せず、次のサイクルでユニットテストを作成した後に実装する

リファクタリングを行う

次のサイクルを始める

  • ここまでのサイクルをリズムよく、早く回すことが、TDD では重要。
    • 1サイクルが短いほど、ミスした時の手戻りが少ないから。
  • 目安としては 10 から 15 分を目安に。
  • どれだけサイクルを回せばよいかについては、コストとの相談。

16.3 Calculator クラスのテスト駆動開発

実装しつつ、前述の事項を確認する。

テストファースト

コンパイルエラーの解決

仮実装

リファクタリング

三角測量

16.4 テスト駆動開発の目的

テスト駆動開発の目的は、プログラマの不安を軽減すること。

ステップバイステップ

  • テスト駆動開発の基本は、「小さく」「1つずつ」
    • 自分を過信してたくさんのことを一気にやろうとするべからず

自分が最初のユーザ

  • テスト対象となるクラス、メソッドを使って自分が最初にテストコードを書く
    • 使いにくいことに、早い段階で気づけるチャンスが有る

動作するきれいなコード

  • TDD では、はじめにテストケースを作成し、動作するプロダクションコードはなにはともあれ、まず作成する。
  • はじめはきれいなコードではないが、リファクタリングにより、美しいコードとすることを目指す。
    • 完璧な設計をはじめから行うことはできないか、あるいは非常にコストがかかる、との知見から来ている。

素早いフィードバック

  • TDD と CI をプロダクション組み合わせることで、素早いフィードバックが保証される。

メンテナンスされたドキュメント

  • テストケースは、テスト対象メソッドの具体的な仕様と、ユースケースを示している。
  • TDD が正しく回っていれば、仕様書やドキュメントが万一おざなりにされるような状況であっても、テストコードという形でのメンテナンスされたドキュメントは保証される。

Eclipse が起動しなくなったので eclipse.ini の pleiades.jar のパスを、絶対指定にした

Eclipseが起動しなくなった。
スプラッシュウィンドウすら出ない。
Pleiades の日本語化適用しているので、eclipse.ini から日本語化適用設定を外してみる。
おなじみのケプラーさんのスプラッシュウィンドウが表示された。

ということで、原因はこいつ。
相対パスでなく、絶対パスで指定することにした。

-javaagent:C:\pleiades\eclipse\features\dropins\MergeDoc\eclipse\plugins\jp.sourceforge.mergedoc.pleiades\pleiades.jar

問題なく起動。

明確な原因は不明であるが、直前にJDKインストールしたから、そのせいかも・・・

JUnit 実践入門 体系的に学ぶユニットテストの技法 付録A 開発環境のセットアップ

付録A 開発環境のセットアップ

A.1 JDK のセットアップ

A.2 Eclipse のセットアップ

JUnit実践入門 ~体系的に学ぶユニットテストの技法 (WEB+DB PRESS plus)

JUnit実践入門 ~体系的に学ぶユニットテストの技法 (WEB+DB PRESS plus)

明解 Javaによるアルゴリズムとデータ構造 6-4_単純挿入ソート

6-4 単純選択ソート straight insertion sort

  • 『原列の先頭要素を、目的列内の適切な位置に挿入』を、n - 1 回繰り返す

    • トランプのカード並べに似たソート(7並べをイメージしてね)
  • 実装としては、

    1. 『左隣の要素が、現在着目している要素より大きい限り、その値を代入』。 tmp に a[i] を保持しつつ、繰り返し用変数 j に i - 1 を代入しておき、以下のいずれか一方を満たせば終了。
      1. 目的列の左端に達する
      2. tmpと等しいか小さいキーを持つ a[j] に出会う
    2. ド・モルガンの法則から、以下2つの条件が両方成立しているあいだ繰り返す
      1. j が 0 より大きい
      2. a[j - 1] の値が tmp より大きい
    3. 操作が完了すると、a[j] に tmp を単純挿入する
  • 飛び越えた要素の交換は実施しないので、安定なアルゴリズム

  • 比較回数: 注目要素を目的列に挿入する際の、平均比較回数
    \displaystyle \sum_{k=1}^{n-1} \frac{k}{2}} = \frac{n(n-1)}{4}

疑問

本だと 、時間計算量
\displaystyle \frac{{n}^2}{2}}
なんだけど、どういう計算なんだろう。

ソースコードと実行結果

import java.util.Scanner;

class InsertionSort {
    static void insertionSort(int[] a, int n) {
        int ccnt = 0; // 比較回数
        int scnt = 0; // 挿入回数
        for (int i = 1; i < n; i++) {
            System.out.println("パス: " + i);
            for (int e : a) {
                System.out.printf("%3d%c", e, (e == a[i]) ? '*' : ' '); // '*' -> ソートする要素
            }
            System.out.println();

            int j;
            int tmp = a[i];

            for (j = i; j > 0 && a[j - 1] > tmp; j--) {
                a[j] = a[j - 1];
                scnt++;
                for (int e : a) {
                    System.out.printf("%3d%c", e, 
                                        (e == a[j - 1]) ? '+'           // '+' -> コピーされる要素
                                        : (e == a[j]) ? '-' : ' ');     // '-' -> 上書かれる要素
                }
                System.out.println();
                ccnt++;
            }
            a[j] = tmp;
            for (int e : a) {
                System.out.printf("%3d%c", e, ' ');
            }
            System.out.println();
            
            System.out.println("比較回数: " + ccnt);
            System.out.println("交換回数: " + scnt);
        }
    }

    public static void main(String[] args) {
        Scanner stdIn = new Scanner(System.in);
        System.out.println("単純挿入ソート");
        System.out.print("要素数: ");

        int nx = stdIn.nextInt();
        int[] x = new int[nx];

        for (int i = 0; i < nx; i++) {
            System.out.print("x[" + i + "]: ");
            x[i] = stdIn.nextInt();
        }

        insertionSort(x, nx);

        System.out.println("昇順にソートしました");
        for (int i = 0; i < nx; i++) {
            System.out.println("x[" + i + "]=" + x[i]);
        }
    }
}
単純挿入ソート
要素数: 4
x[0]: 4
x[1]: 3
x[2]: 2
x[3]: 1
パス: 1
  4   3*  2   1 
  4+  4+  2   1 
  3   4   2   1 
比較回数: 1
交換回数: 1
パス: 2
  3   4   2*  1 
  3   4+  4+  1 
  3+  3+  4   1 
  2   3   4   1 
比較回数: 3
交換回数: 3
パス: 3
  2   3   4   1*
  2   3   4+  4+
  2   3+  3+  4 
  2+  2+  3   4 
  1   2   3   4 
比較回数: 6
交換回数: 6
昇順にソートしました
x[0]=1
x[1]=2
x[2]=3
x[3]=4
  • 番兵法 sentinel を適用して、終了条件をひとつにした版のメソッド
class InsertionSortSen {
    static void insertionSort(int[] a, int n) {
        int ccnt = 0; // 比較回数
        int scnt = 0; // 挿入回数
        for (int i = 2; i < n; i++) {
            System.out.println("パス: " + i);
            for (int e : a) {
                System.out.printf("%3d%c", e, (e == a[i]) ? '*' : ' '); // '*' -> ソートする要素
            }
            System.out.println();

            int j;
            int tmp = a[i] = a[0];

            for (j = i; a[j - 1] > tmp; j--) {
                a[j] = a[j - 1];
                scnt++;
                for (int e : a) {
                    System.out.printf("%3d%c", e, 
                                        (e == a[j - 1]) ? '+'           // '+' -> コピーされる要素
                                        : (e == a[j]) ? '-' : ' ');     // '-' -> 上書かれる要素
                }
                System.out.println();
                ccnt++;
            }
            if (j > 0) {
                a[j] = tmp;
            }
            for (int e : a) {
                System.out.printf("%3d%c", e, ' ');
            }
            System.out.println();
            
            System.out.println("比較回数: " + ccnt);
            System.out.println("交換回数: " + scnt);
        }
    }
}

2分挿入ソート binary insertion sort

  • 配列が大きくなるにつれ、要素の挿入に要する比較・代入のコストも大きくなる。挿入すべき位置の探索に、二分探索を使うことでコスト削減を実現している。
    • 代入に関しては軽くなってない。

ソースコード

import java.util.Scanner;

class BinaryInsertionSort {
    static void insertionSort(int[] a, int n) {
        for (int i = 1; i < n; i++) {
            System.out.println("パス: " + i);

            int j = binarySearch(a, a[i], i - 1);
            int tmp = a[i];
            
            for (int k = 0; k < n; k++) {
                System.out.printf("%3d%c", a[k],
                    (k < i) ? '-'           // '-' -> 目的列
                    : (k == i) ? '*'        // '*' -> ソート対象
                    : ' ');                 // ' ' -> 原列
            }
            System.out.println();
            
            for (int k = i; k > j; k--) {
                a[k] = a[k - 1];
            }
            
            a[j] = tmp;
            for (int k = 0; k < n; k++) {
                System.out.printf("%3d%c", a[k],
                    (k < j) ? '-'           // '-' -> 目的列
                    : (k == j) ? '*'        // '-' -> ソート対象の挿入先
                    : (k < i + 1) ? '-'     // '-' -> 目的列
                    : ' ');                 // ' ' -> 原列
            }
            System.out.println();
        }
    }

    /**
     * ある要素が、既存の昇順ソート済int配列のどのインデックスに挿入されるべきか、あるべきインデックスを返すメソッド
     * 
     * @param a
     * @param key
     * @param tailIdx
     * @return 挿入すべきインデックス
     */
    static int binarySearch(int[] a, int key, int tailIdx) {
        int headIdx = 0;
        int centerIdx = -1;
        int index = -1;
        
        while (headIdx <= tailIdx) {
            centerIdx = (headIdx + tailIdx) / 2;
            if (a[centerIdx] == key) {
                index = centerIdx + 1;          // 既に同値が存在すれば、次のインデックスを返す
                break;
            } else if (key < a[centerIdx]) {    // keyが前方に存在
                tailIdx = centerIdx - 1;
                index = centerIdx;              // 現在のcenterIdxを代入
            } else {                            // keyが後方に存在
                headIdx = centerIdx + 1;
                index = headIdx;                // 次のheadIdxを代入
            }
        }
        return index;
    }

    public static void main(String[] args) {
        Scanner stdIn = new Scanner(System.in);
        System.out.println("2分挿入ソート");
        System.out.print("要素数: ");

        int nx = stdIn.nextInt();
        int[] x = new int[nx];

        for (int i = 0; i < nx; i++) {
            System.out.print("x[" + i + "]: ");
            x[i] = stdIn.nextInt();
        }

        insertionSort(x, nx);

        System.out.println("昇順にソートしました");
        System.out.println("-------------");
        for (int i = 0; i < nx; i++) {
            System.out.println("x[" + i + "]=" + x[i]);
        }
    }
}
  • 実行結果

2分挿入ソート
要素数: 10
x[0]: 9
x[1]: 1
x[2]: 4
x[3]: 5
x[4]: 3
x[5]: 8
x[6]: 4
x[7]: 58
x[8]: 7
x[9]: 6
パス: 1
  9-  1*  4   5   3   8   4  58   7   6 
  1*  9-  4   5   3   8   4  58   7   6 
パス: 2
  1-  9-  4*  5   3   8   4  58   7   6 
  1-  4*  9-  5   3   8   4  58   7   6 
パス: 3
  1-  4-  9-  5*  3   8   4  58   7   6 
  1-  4-  5*  9-  3   8   4  58   7   6 
パス: 4
  1-  4-  5-  9-  3*  8   4  58   7   6 
  1-  3*  4-  5-  9-  8   4  58   7   6 
パス: 5
  1-  3-  4-  5-  9-  8*  4  58   7   6 
  1-  3-  4-  5-  8*  9-  4  58   7   6 
パス: 6
  1-  3-  4-  5-  8-  9-  4* 58   7   6 
  1-  3-  4-  4*  5-  8-  9- 58   7   6 
パス: 7
  1-  3-  4-  4-  5-  8-  9- 58*  7   6 
  1-  3-  4-  4-  5-  8-  9- 58*  7   6 
パス: 8
  1-  3-  4-  4-  5-  8-  9- 58-  7*  6 
  1-  3-  4-  4-  5-  7*  8-  9- 58-  6 
パス: 9
  1-  3-  4-  4-  5-  7-  8-  9- 58-  6*
  1-  3-  4-  4-  5-  6*  7-  8-  9- 58-
昇順にソートしました
-------------
x[0]=1
x[1]=3
x[2]=4
x[3]=4
x[4]=5
x[5]=6
x[6]=7
x[7]=8
x[8]=9
x[9]=58

疑問

  • テキストだと、2分挿入ソートは安定ではないと書いているが、安定性を保持したまま実装できてる気がする
    • なんか間違えてる?

回答

  • 実装によりけりっぽい。ちょっと、テキストさん、断言せんといてや・・・

    挿入ソートの改良で、挿入するデータの前ではソートが済んでいるという性質を利用して、挿入する箇所を二分探索するというものである。データの量が少ないときにはあまり効果がないが、多いときには比較回数が少なくなる。探索アルゴリズムによっては不安定なソートになるが、工夫により安定させることが可能である。
    挿入ソート - Wikipedia

明解 Javaによるアルゴリズムとデータ構造

明解 Javaによるアルゴリズムとデータ構造

2014年書き初めという体で、Markdown 記法でエントリを書いてみる

このエントリの目的

気づいてなかったが、はてなブログが Markdown 記法に対応してた。
-> Markdown記法に対応しました - はてなブログ開発ブログ

Markdown 記法で書かれたドキュメントを見る機会はあるが、自分で書いてなかったので、慣れるついでに書いてみる。

このエントリ自体、Markdown 記法で記述されてるよって意味ね。

そもそも Markdown 記法ってなんぞや

ということで、こちらを読んだ。すげーわかりやすくてありがたいっすー。やったね、たえちゃん!
-> blog::2310 » Markdown文法の全訳

はてな記法とかと同じような思想で、装飾されたテキストをいかに簡便な記述から出力するか、ということに注力された記法という理解。はてな記法ともイメージが近くて、そんなに混乱することはなさそう。これが複数の媒体で横断的に使えるんだから、ありがとうってかんじやね。標準化バンザイ。

よく使いそうな記法をピックアップ

Markdown記法に対応しました - はてなブログ開発ブログ からところどころ抜粋。

見出し

改行

Markdownで<br \/>タグを使用したい時は、その行の末尾を二つ以上のスペースを記述してから改行することとなります。

引用

Markdownで引用を表現するときにはEメールと同じ方法、>を用います。 もしあなたがEメールで引用をすることになじんでいるのであればMarkdownでの使用は容易です。 あなたは既にルールを知っています。 改行した各行の冒頭に>をつけるだけです。

罫線

3つ以上のハイフン(‘-’)やアスタリスク(‘*’)、アンダースコア(‘_’)だけで構成されている行は罫線<hr />となります。 また、これらの記号の間には半角スペースを用いることができます。

リスト

  • はてな記法と同じ。つまり "-" や "+" を使えばよい。ただし、スペース(タブ)必須かつ、その数には意味があるので要注意。
    • ネストしたい場合、はてな記法みたいに"--"ではなく、" -"みたいになるのに注意。
    • 番号付きリストは、"1." とか "2." とか。

      リストの番号もしくは記号は通常左端からはじまりますが、冒頭に3つのスペー スまでは許されています。またリストの番号もしくは記号の後には1つ以上の スペースか、タブが挿入されていなければいけません。
      リストを綺麗に見せるために、リストの内容の二行目以降を揃えることができます。

その他

  • htmlタグ等も解釈されてしまうので、タグ自体を表示させたいときは適宜エスケープするなりの処理が必要。

まとめ

ひとまずこんだけ知ってれば、はてなブログ書くのにズボラする分には、問題なさげじゃなかろうか。想像より楽ちんだった。

ソース

#このエントリの目的
気づいてなかったが、はてなブログが Markdown 記法に対応してた。  
-> [http://staff.hatenablog.com/entry/2012/09/19/153219:title]
  
Markdown 記法で書かれたドキュメントを見る機会はあるが、自分で書いてなかったので、慣れるついでに書いてみる。  
  
このエントリ自体、Markdown 記法で記述されてるよって意味ね。

#そもそも Markdown 記法ってなんぞや
ということで、こちらを読んだ。すげーわかりやすくてありがたいっすー。やったね、たえちゃん!  
-> [http://blog.2310.net/archives/6:title]
  
はてな記法とかと同じような思想で、装飾されたテキストをいかに簡便な記述から出力するか、ということに注力された記法という理解。はてな記法ともイメージが近くて、そんなに混乱することはなさそう。これが複数の媒体で横断的に使えるんだから、ありがとうってかんじやね。標準化バンザイ。

#よく使いそうな記法をピックアップ
[http://staff.hatenablog.com/entry/2012/09/19/153219:title] からところどころ抜粋。
  
##見出し
- "#" でよし。はてな記法でいうところの、"*"感覚。

##改行
>Markdownで
タグを使用したい時は、その行の末尾を二つ以上のスペースを記述してから改行することとなります。 ##引用 >Markdownで引用を表現するときにはEメールと同じ方法、>を用います。 もしあなたがEメールで引用をすることになじんでいるのであればMarkdownでの使用は容易です。 あなたは既にルールを知っています。 改行した各行の冒頭に>をつけるだけです。 ##罫線 >3つ以上のハイフン(‘-’)やアスタリスク(‘*’)、アンダースコア(‘_’)だけで構成されている行は罫線\
となります。 また、これらの記号の間には半角スペースを用いることができます。 ##リスト - はてな記法と同じ。bullet は、 "-" でも "+" でも "*" でもよい。ただし、スペース(タブ)必須かつ、その数には意味があるので要注意。 - ネストしたい場合、はてな記法みたいに"--"ではなく、" -"みたいになるのに注意。 - 番号付きリストは、"1." とか "2." とか。 >リストの番号もしくは記号は通常左端からはじまりますが、冒頭に3つのスペー スまでは許されています。またリストの番号もしくは記号の後には1つ以上の スペースか、タブが挿入されていなければいけません。 リストを綺麗に見せるために、リストの内容の二行目以降を揃えることができます。 ##その他 - htmlタグ等も解釈されてしまうので、タグ自体を表示させたいときは適宜エスケープするなりの処理が必要。 # まとめ ひとまずこんだけ知ってれば、はてなブログ書くのにズボラする分には、問題なさげじゃなかろうか。想像より楽ちんだった。