Programming By Example のためのちいさなライブラリ

  表形式で事前条件と事後条件の列を並べた動作例を1件以上用意して1件ずつ検証し、その結果をまとめてアサートするのにちょっと便利なライブラリ Tester を作ってみた。

  データの(空行でない)先頭行に「事前条件」セルと「事後条件」セルを書き、次の行には事前条件の列名のセルを並べた後に事後条件のセルを並べる。その際、事前条件先頭列のすぐ上に前の行の「事前条件」セル、事後条件先頭列のすぐ上に前の行の「事後条件」セルを配置する。3行目以降は各列名に沿った値例を記述する。ここまでが準備。

  Tester.Evaluateメソッドでテストコードを評価する。このメソッドは、上で用意した複数行にわたる表形式の文字列を第一引数にとり、第二引数には、Tester.TestCaseクラスを受け取るAction(ラムダ式)の形でテスト評価処理をとる。その際、テストケースの値をValueOfメソッドで参照できるので、下記のコード例のように書くことができる。各行のテストケースデータについてのテスト評価結果がすべて合格(成功)ならなにも起きない。1行以上のテストケースが不合格なら全テストケースデータの結果の文字列を含む TestCaseFailedException が発生するのでテストメソッドは不合格になる。

  制約事項:
   表のセルデータ内には表のセル区切り文字列と行区切り文字列を含めることはできない。
   ただし、表のセル区切り文字列(既定では"|")、表の行区切り文字列(既定では"\r\n")には任意の文字列を指定できる。
   そのため、区切り文字列の指定を調整すれば、たとえばExcelで作った表をテストコードにコピー&ペーストしてもいい(はず)。

使い方と挙動は、テストコードを見るとわかるかと思う。

using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Collections.Generic;

namespace Tester.Tests
{
    [TestClass()]
    public class TesterTests
    {
        [TestMethod()]
        public void EvaluateTest()
        {
            try
            {
                Tester.Evaluate(@"
|事前条件||事後条件
備考|x1|x2|y=x1+x2
負|-2|-1|-3
零|0|0|0
正|5|1|6
", (testCase) => {
                    string note = testCase.ValueOf("", "備考");
                    int x1 = int.Parse(testCase.ValueOf("事前条件", "x1"));
                    int x2 = int.Parse(testCase.ValueOf("事前条件", "x2"));
                    int expected = int.Parse(testCase.ValueOf("事後条件", "y=x1+x2"));
                    Assert.AreEqual(expected, x1 + x2, note);
                }, "|", "\r\n");
            }
            catch (Exception ex)
            {
                Assert.Fail(string.Format("例外は発生しないはずです。{0}", ex.ToString()));
            }
        }
        [TestMethod()]
        public void EvaluateTest_Fail()
        {
            try
            {
                Tester.Evaluate(@"
|事前条件||事後条件
備考|x1|x2|y=x1+x2
負|-2|-1|-3
零(故意の誤り)|0|0|1
正(故意の誤り)|5|1|0
", (testCase) => {
                    string note = testCase.ValueOf("", "備考");
                    int x1 = int.Parse(testCase.ValueOf("事前条件", "x1"));
                    int x2 = int.Parse(testCase.ValueOf("事前条件", "x2"));
                    int expected = int.Parse(testCase.ValueOf("事後条件", "y=x1+x2"));
                    Assert.AreEqual(expected, x1 + x2, note);
                }, "|", "\r\n");
            }
            catch (TestFailedException ex)
            {
                Assert.AreEqual(@"0:成功
1:Assert.AreEqual に失敗しました。<1> が必要ですが、<0> が指定されました。零(故意の誤り)
2:Assert.AreEqual に失敗しました。<0> が必要ですが、<6> が指定されました。正(故意の誤り)
", ex.Message);
            }
        }
        [TestMethod()]
        public void EvaluateTest_テストケースカテゴリ参照誤り()
        {
            try
            {
                Tester.Evaluate(@"
|事前条件||事後条件
備考|x1|x2|y=x1+x2
負|-2|-1|-3
零|0|0|0
正|5|1|6
", (testCase) => {
                    string note = testCase.ValueOf("", "備考");
                    int x1 = int.Parse(testCase.ValueOf("事前条件", "x1"));
                    int x2 = int.Parse(testCase.ValueOf("事前条件", "x2"));
                    int expected = int.Parse(testCase.ValueOf("事前条件", "y=x1+x2"));//正しくは"事後条件"
                    Assert.AreEqual(expected, x1 + x2, note);
                }, "|", "\r\n");
                Assert.Fail(string.Format("ここまでは到達しないはずです。"));
            }
            catch (TestFailedException ex)
            {
                Assert.Fail(string.Format("この種類の例外は検出されないはずです。{0}", ex.ToString()));
            }
            catch (TestErrorException ex)
            {
                Assert.AreEqual(@"ValueOf(事前条件,y=x1+x2)が呼び出されましたが、カテゴリ 事前条件 には列名 y=x1+x2 が見つかりません。", ex.Message);
                Assert.IsInstanceOfType(ex, typeof(TestCaseColumnNameNotFoundException));
            }
        }
        [TestMethod()]
        public void EvaluateTest_テストケース列参照誤り()
        {
            try
            {
                Tester.Evaluate(@"
|事前条件||事後条件
備考|x1|x2|y=x1+x2
負|-2|-1|-3
零|0|0|0
正|5|1|6
", (testCase) => {
                    string note = testCase.ValueOf("", "備考");
                    int x1 = int.Parse(testCase.ValueOf("事前条件", "x"));//正しくは x1
                    int x2 = int.Parse(testCase.ValueOf("事前条件", "x2"));
                    int expected = int.Parse(testCase.ValueOf("事後条件", "y=x1+x2"));
                    Assert.AreEqual(expected, x1 + x2, note);
                }, "|", "\r\n");
                Assert.Fail(string.Format("ここまでは到達しないはずです。"));
            }
            catch (TestFailedException ex)
            {
                Assert.Fail(string.Format("この種類の例外は検出されないはずです。{0}", ex.ToString()));
            }
            catch (TestErrorException ex)
            {
                Assert.AreEqual(@"ValueOf(事前条件,x)が呼び出されましたが、カテゴリ 事前条件 には列名 x が見つかりません。", ex.Message);
                Assert.IsInstanceOfType(ex, typeof(TestCaseColumnNameNotFoundException));
            }
        }
        [TestMethod()]
        public void EvaluateTest_未定義のテストケースカテゴリ名参照()
        {
            try
            {
                Tester.Evaluate(@"
|事前条件||事後条件
備考|x1|x2|y=x1+x2
負|-2|-1|-3
零|0|0|0
正|5|1|6
", (testCase) => {
                    string note = testCase.ValueOf("", "備考");
                    int x1 = int.Parse(testCase.ValueOf("事前条件", "x1"));
                    int x2 = int.Parse(testCase.ValueOf("事前条件", "x2"));
                    int expected = int.Parse(testCase.ValueOf("誤記", "y=x1+x2"));//正しくは 事後条件
                    Assert.AreEqual(expected, x1 + x2, note);
                }, "|", "\r\n");
                Assert.Fail(string.Format("ここまでは到達しないはずです。"));
            }
            catch (TestFailedException ex)
            {
                Assert.Fail(string.Format("この種類の例外は検出されないはずです。{0}", ex.ToString()));
            }
            catch (TestErrorException ex)
            {
                Assert.AreEqual(@"ValueOf(誤記,y=x1+x2)が呼び出されましたが、カテゴリ 誤記 が見つかりません。", ex.Message);
            }
        }

        [TestMethod()]
        public void ParseTestCaseListTest_0ケースの例_テストケースなし()
        {
            try
            {
                List<TestCase> testCases;
                testCases = Tester.ParseTestCaseList(@"
事前条件|事後条件
x|!x
");
                Assert.Fail("例外が発生するはずです。");
            }
            catch (TestErrorException ex)
            {
                Assert.AreEqual("テストケースがありません。列名定義 x|!x に沿って、1行以上のテストケースのデータ行が必要です。", ex.Message);
                Assert.IsInstanceOfType(ex, typeof(TestCaseEmptyException));
            }
        }
        [TestMethod()]
        public void ParseTestCaseListTest_0_列名なし()
        {
            List<TestCase> testCases;
            testCases = Tester.ParseTestCaseList(@"
事前条件|事後条件
");
            Assert.AreEqual(0, testCases.Count);
        }

        [TestMethod()]
        public void ParseTestCaseListTest_2ケースの例()
        {
            List<TestCase> testCases;
            testCases = Tester.ParseTestCaseList(@"
事前条件|事後条件
x|y
0|1
2|3
");
            Assert.AreEqual(2, testCases.Count);
            Assert.AreEqual("0", testCases[0].ValueOf("事前条件", "x"));
            Assert.AreEqual("1", testCases[0].ValueOf("事後条件", "y"));
            Assert.AreEqual("2", testCases[1].ValueOf("事前条件", "x"));
            Assert.AreEqual("3", testCases[1].ValueOf("事後条件", "y"));
        }

        [TestMethod()]
        public void ParseTestCaseListTest_3ケースの例_事前条件の前の列あり()
        {
            List<TestCase> testCases;
            testCases = Tester.ParseTestCaseList(@"
|事前条件||事後条件
備考|x1|x2|y=x1+x2
負|-2|-1|-3
零|0|0|0
正|5|1|6
");
            Assert.AreEqual(3, testCases.Count);
            Assert.AreEqual("負", testCases[0].ValueOf("", "備考"));
            Assert.AreEqual("-2", testCases[0].ValueOf("事前条件", "x1"));
            Assert.AreEqual("-1", testCases[0].ValueOf("事前条件", "x2"));
            Assert.AreEqual("-3", testCases[0].ValueOf("事後条件", "y=x1+x2"));
            Assert.AreEqual("零", testCases[1].ValueOf("", "備考"));
            Assert.AreEqual("0", testCases[1].ValueOf("事前条件", "x1"));
            Assert.AreEqual("0", testCases[1].ValueOf("事前条件", "x2"));
            Assert.AreEqual("0", testCases[1].ValueOf("事後条件", "y=x1+x2"));
            Assert.AreEqual("正", testCases[2].ValueOf("", "備考"));
            Assert.AreEqual("5", testCases[2].ValueOf("事前条件", "x1"));
            Assert.AreEqual("1", testCases[2].ValueOf("事前条件", "x2"));
            Assert.AreEqual("6", testCases[2].ValueOf("事後条件", "y=x1+x2"));
        }
        [TestMethod()]
        public void ParseTestCaseListTest_セル区切り文字変更()
        {
            List<TestCase> testCases;
            testCases = Tester.ParseTestCaseList(@"
事前条件,事後条件
x,y
0,1
2,3
", ",");
            Assert.AreEqual(2, testCases.Count);
            Assert.AreEqual("0", testCases[0].ValueOf("事前条件", "x"));
            Assert.AreEqual("1", testCases[0].ValueOf("事後条件", "y"));
            Assert.AreEqual("2", testCases[1].ValueOf("事前条件", "x"));
            Assert.AreEqual("3", testCases[1].ValueOf("事後条件", "y"));
        }
        [TestMethod()]
        public void ParseTestCaseListTest_行区切り文字変更()
        {
            List<TestCase> testCases;
            testCases = Tester.ParseTestCaseList(@"@事前条件|事後条件@x|y@0|1@2|3@", "|", "@");
            Assert.AreEqual(2, testCases.Count);
            Assert.AreEqual("0", testCases[0].ValueOf("事前条件", "x"));
            Assert.AreEqual("1", testCases[0].ValueOf("事後条件", "y"));
            Assert.AreEqual("2", testCases[1].ValueOf("事前条件", "x"));
            Assert.AreEqual("3", testCases[1].ValueOf("事後条件", "y"));
        }
        [TestMethod()]
        public void ParseTestCaseListTest_事前条件事後条件のテキスト変更()
        {
            List<TestCase> testCases;
            testCases = Tester.ParseTestCaseList(@"
pre-condition,post-condition
x,y
0,1
2,3
", ",", null, new string[] { "pre-condition", "post-condition" });
            Assert.AreEqual(2, testCases.Count);
            Assert.AreEqual("0", testCases[0].ValueOf("pre-condition", "x"));
            Assert.AreEqual("1", testCases[0].ValueOf("post-condition", "y"));
            Assert.AreEqual("2", testCases[1].ValueOf("pre-condition", "x"));
            Assert.AreEqual("3", testCases[1].ValueOf("post-condition", "y"));
        }
        [TestMethod()]
        public void ParseTestCaseListTest_事前条件事後条件の欠損()
        {
            try
            {
                List<TestCase> testCases;
                testCases = Tester.ParseTestCaseList(@"
x|y
0|1
2|3
");
                Assert.Fail("例外が発生するはずです。");
            }
            catch (TestErrorException ex)
            {
                Assert.AreEqual("テストケース定義の書式に誤りがあります。テストケース行とそれらの前の列名の行に先行して「事前条件」と「事後条件」を含む行が必要です。", ex.Message);
                Assert.IsInstanceOfType(ex, typeof(TestCaseCategoriesMissingException));
            }
        }
        [TestMethod()]
        public void ParseTestCaseListTest_列名行の欠損()
        {
            try
            {
                List<TestCase> testCases;
                testCases = Tester.ParseTestCaseList(@"
事前条件|事後条件
0|1
2|3
");
                Assert.AreEqual("0", testCases[0].ValueOf("事前条件", "x"));
                Assert.AreEqual("1", testCases[0].ValueOf("事後条件", "y"));
                Assert.AreEqual("2", testCases[1].ValueOf("事前条件", "x"));
                Assert.AreEqual("3", testCases[1].ValueOf("事後条件", "y"));
                Assert.Fail("例外が発生するはずです。");
            }
            catch (TestErrorException ex)
            {
                Assert.AreEqual(@"ValueOf(事前条件,x)が呼び出されましたが、カテゴリ 事前条件 には列名 x が見つかりません。", ex.Message);
                Assert.IsInstanceOfType(ex, typeof(TestCaseColumnNameNotFoundException));
            }
        }
        [TestMethod()]
        public void ParseTestCaseListTest_事前条件事後条件のカテゴリ名変更誤り()
        {
            try
            {
                List<TestCase> testCases;
                testCases = Tester.ParseTestCaseList(@"
事前条件|事後条件
x|y
0|1
2|3
", null, null, new string[] { });
                Assert.Fail("例外が発生するはずです。");
            }
            catch (TestErrorException ex)
            {
                Assert.AreEqual(@"事前条件のカテゴリ名と事後条件のカテゴリ名を持つ2要素の文字列配列が必要ですが、0要素の配列 {} が指定されました。", ex.Message);
                Assert.IsInstanceOfType(ex, typeof(TestCaseCategoriesReplacementException));
            }
        }
    }
}

上のテストコードに合格するコードはこんな感じ。

using System;
using System.Collections.Generic;
using System.Runtime.Serialization;

namespace Tester
{
    public class Tester
    {
        public static readonly string DefaultRowSeparator = "\r\n";
        public static readonly string DefaultCellSeparator = "|";
        public static readonly string DefaultPreConditionLabelText = "事前条件";
        public static readonly string DefaultPostConditionLabelText = "事後条件";
        public string RowSeparator = DefaultRowSeparator;
        public string CellSeparator = DefaultCellSeparator;
        public string PreConditionLabelText = DefaultPreConditionLabelText;
        public string PostConditionLabelText = DefaultPostConditionLabelText;

        public Tester(string cellSeparator = null, string rowSeparator = null, string[] prePostStrings = null)
        {
            CellSeparator = (null == cellSeparator) ? DefaultCellSeparator : cellSeparator;
            RowSeparator = (null == rowSeparator) ? DefaultRowSeparator : rowSeparator;
            if (null != prePostStrings && 2 != prePostStrings.Length)
            {
                throw new TestCaseCategoriesReplacementException(prePostStrings);
            }
            PreConditionLabelText = (null == prePostStrings) ? DefaultPreConditionLabelText : prePostStrings[0];
            PostConditionLabelText = (null == prePostStrings) ? DefaultPostConditionLabelText : prePostStrings[1];
        }

        public static void Evaluate(string testCases, Action<TestCase> testCode, string cellSeparator = null, string rowSeparator = null, string[] prePostStrings = null)
        {
            Tester tester = new Tester(cellSeparator, rowSeparator, prePostStrings);
            tester.Evaluate(testCases, testCode);
        }
        public static List<TestCase> ParseTestCaseList(string testCases, string cellSeparator = null, string rowSeparator = null, string[] prePostStrings = null)
        {
            Tester tester = new Tester(cellSeparator, rowSeparator, prePostStrings);
            return tester.ParseTestCaseList(testCases);
        }

        private void Evaluate(string testCases, Action<TestCase> testCode)
        {
            var cases = this.ParseTestCaseList(testCases);
            var actuals = new List<TestResult>();
            bool hasFailures = false;
            foreach (var testCase in cases)
            {
                var actual = new TestResult();
                try
                {
                    testCode(testCase);
                    actuals.Add(actual.Success());
                }
                catch (TestErrorException)
                {
                    throw;
                }
                catch (Exception ex)
                {
                    actuals.Add(actual.Fail(ex.Message));
                    hasFailures = true;
                }
            }
            if (hasFailures)
            {
                throw new TestFailedException(actuals);
            }
        }

        private List<TestCase> ParseTestCaseList(string testCases)
        {
            var cases = new List<TestCase>();
            int indexPreCond = -1;
            int indexPostCond = -1;
            var columnNames = new Dictionary<int, string>();
            string[] lines = testCases.Split(new string[] { this.RowSeparator }, StringSplitOptions.None);
            for (int r = 0; r < lines.Length; ++r)
            {
                string line = lines[r];
                if (string.IsNullOrWhiteSpace(line))
                {
                    continue;
                }
                if (0 == columnNames.Count)
                {
                    if (!(0 <= indexPreCond && 0 <= indexPostCond))
                    {
                        FindPrePostConditionIndexes(ref indexPreCond, ref indexPostCond, lines, r);
                    }
                    else
                    {
                        MemoryColumnNames(columnNames, lines, r);
                    }
                }
                else
                {
                    SetCellValues(cases, indexPreCond, indexPostCond, columnNames, lines, r);
                }
            }
            if (0 == cases.Count)
            {
                if (!(0 <= indexPreCond && 0 <= indexPostCond))
                {
                    throw new TestCaseCategoriesMissingException(this.PreConditionLabelText, this.PostConditionLabelText);
                }
                if (0 < columnNames.Count)
                {
                    throw new TestCaseEmptyException(this.GetColumnNamesDefinition(columnNames));
                }
            }
            return cases;
        }

        private string GetColumnNamesDefinition(Dictionary<int, string> columnNames)
        {
            var names = new List<string>();
            for (int i = 0; i < columnNames.Count; ++i)
            {
                names.Add(columnNames[i]);
            }
            return string.Join(this.CellSeparator, names.ToArray());
        }

        private void MemoryColumnNames(Dictionary<int, string> columnNames, string[] lines, int r)
        {
            string[] cells = lines[r].Split(new string[] { this.CellSeparator }, StringSplitOptions.None);
            for (int c = 0; c < cells.Length; ++c)
            {
                var cell = cells[c];
                columnNames.Add(c, cell);
            }
        }

        private void FindPrePostConditionIndexes(ref int indexPreCond, ref int indexPostCond, string[] lines, int r)
        {
            string[] cells = lines[r].Split(new string[] { this.CellSeparator }, StringSplitOptions.None);
            for (int c = 0; c < cells.Length; ++c)
            {
                var cell = cells[c];
                if (this.PreConditionLabelText.Equals(cell))
                {
                    indexPreCond = c;
                }
                if (this.PostConditionLabelText.Equals(cell))
                {
                    indexPostCond = c;
                }
            }
        }

        private void SetCellValues(List<TestCase> cases, int indexPreCond, int indexPostCond, Dictionary<int, string> columnNames, string[] lines, int r)
        {
            var dic = new Dictionary<string, Dictionary<string, string>>();
            dic[""] = new Dictionary<string, string>();
            dic[this.PreConditionLabelText] = new Dictionary<string, string>();
            dic[this.PostConditionLabelText] = new Dictionary<string, string>();
            string[] cells = lines[r].Split(new string[] { this.CellSeparator }, StringSplitOptions.None);
            for (int c = 0; c < cells.Length; ++c)
            {
                string categoryName = GetCategoryName(c, indexPreCond, indexPostCond);
                string columnName = columnNames[c];
                string value = cells[c];
                dic[categoryName].Add(columnName, value);
            }
            cases.Add(new TestCase(dic));
        }

        private string GetCategoryName(int c, int indexPreCond, int indexPostCond)
        {
            if (c < indexPreCond)
            {
                return "";
            }
            if (c < indexPostCond)
            {
                return this.PreConditionLabelText;
            }
            return this.PostConditionLabelText;
        }
    }
    public class TestResult
    {
        public static string SUCCESS = "成功";
        public string Result { get; set; }

        internal TestResult Success()
        {
            this.Result = SUCCESS;
            return this;
        }

        internal TestResult Fail(string message)
        {
            this.Result = message;
            return this;
        }

        public override bool Equals(object obj)
        {
            if (obj == null)
            {
                return false;
            }
            if (!obj.GetType().Equals(this.GetType()))
            {
                return false;
            }
            return this.ToString().Equals(obj.ToString());
        }

        public override string ToString()
        {
            if (null == this.Result)
            {
                return string.Empty;
            }
            return this.Result.ToString();
        }

        public override int GetHashCode()
        {
            return this.Result.GetHashCode();
        }
    }
    public class TestCase
    {
        private Dictionary<string, Dictionary<string, string>> Value;

        public TestCase(Dictionary<string, Dictionary<string, string>> dicTestCases)
        {
            this.Value = dicTestCases;
        }

        public string ValueOf(string categoryName, string columnName)
        {
            if (!this.Value.ContainsKey(categoryName))
            {
                throw new TestCaseCategoryNameNotFoundException(categoryName, columnName);
            }
            if (!this.Value[categoryName].ContainsKey(columnName))
            {
                throw new TestCaseColumnNameNotFoundException(categoryName, columnName);
            }
            return this.Value[categoryName][columnName];
        }
        [Serializable]
        public class TestCaseKeyException : TestErrorException
        {
            public List<TestResult> ActualTestResults { get; private set; }
            public TestCaseKeyException(string message)
                : base(message)
            {
            }
        }
    }
    /// <summary>
    /// テストコード自体の異常の場合の例外。
    /// すなわち、テストケースの失敗ではなく、テストケースライブラリの用法または実装の異常を検出した場合の例外。
    /// </summary>
    [Serializable]
    public class TestErrorException : Exception
    {
        public TestErrorException(string message)
            : base(message)
        {
        }
    }
    /// <summary>
    /// テストコードが失敗した(被験体のコードがテストに不合格となった)場合の例外。
    /// </summary>
    [Serializable]
    public class TestFailedException : Exception
    {
        public List<TestResult> ActualTestResults { get; private set; }
        public TestFailedException(List<TestResult> actuals)
            : base(TestFailedException.Description(actuals))
        {
            this.ActualTestResults = actuals;
        }
        public static string Description(List<TestResult> actuals)
        {
            string description = "";
            for (int i = 0; i < actuals.Count; ++i)
            {
                TestResult actual = actuals[i];
                string value = (null == actual) ? "" : actual.ToString();
                description += string.Format("{0}:{1}\r\n", i, value);
            }
            return description;
        }
    }
    [Serializable]
    public class TestCaseCategoriesReplacementException : TestErrorException
    {
        private string[] prePostStrings;

        public TestCaseCategoriesReplacementException(string[] prePostStrings)
            : base(Description(prePostStrings))
        {
            this.prePostStrings = prePostStrings;
        }
        private static string Description(string[] prePostStrings)
        {
            return string.Format("事前条件のカテゴリ名と事後条件のカテゴリ名を持つ2要素の文字列配列が必要ですが、{0}要素の配列 {1} が指定されました。",
                prePostStrings.Length, string.Format("{{{0}}}", string.Join(",", prePostStrings)));
        }
    }
    [Serializable]
    public class TestCaseCategoryNameNotFoundException : TestErrorException
    {
        public TestCaseCategoryNameNotFoundException(string categoryName, string columnName)
            : base(Description(categoryName, columnName))
        {
        }
        public static string Description(string categoryName, string columnName)
        {
            return string.Format("ValueOf({0},{1})が呼び出されましたが、カテゴリ {0} が見つかりません。",
                categoryName, columnName);
        }
    }
    [Serializable]
    public class TestCaseColumnNameNotFoundException : TestErrorException
    {
        public TestCaseColumnNameNotFoundException(string categoryName, string columnName)
            : base(Description(categoryName, columnName))
        {
        }
        public static string Description(string categoryName, string columnName)
        {
            return string.Format("ValueOf({0},{1})が呼び出されましたが、カテゴリ {0} には列名 {1} が見つかりません。",
                categoryName, columnName);
        }
    }

    [Serializable]
    public class TestCaseEmptyException : TestErrorException
    {
        public TestCaseEmptyException(string columnNamesDefinition)
            : base(Description(columnNamesDefinition))
        {
        }
        public static string Description(string columnNamesDefinition)
        {
            return string.Format("テストケースがありません。列名定義 {0} に沿って、1行以上のテストケースのデータ行が必要です。",
                columnNamesDefinition);
        }
    }

    [Serializable]
    public class TestCaseCategoriesMissingException : TestErrorException
    {
        public TestCaseCategoriesMissingException(string preConditionLabelText, string postConditionLabelText)
            : base(Description(preConditionLabelText, postConditionLabelText))
        {
        }
        public static string Description(string preConditionLabelText, string postConditionLabelText)
        {
            return string.Format("テストケース定義の書式に誤りがあります。テストケース行とそれらの前の列名の行に先行して「{0}」と「{1}」を含む行が必要です。",
                preConditionLabelText, postConditionLabelText);
        }
    }
}

Tester.zip 直

バイナリ形式ファイルを扱うためのクラスライブラリ BinaryFormat.dll を作ってみた。

単体テストコードも書いてみた(BinaryFormatTest.dll)。
サンドキャッスル(SHFB)(別途ダウンロードしてインストールした)で生成したヘルプファイル(documentation.chm)もいちおう付けてみた。

Microsoft VisualStudio 2013 で作ったソリューションのフォルダをZIP圧縮したものを貼っておく(.Net 4向け)。
BinaryFormat.zip 直
MITライセンスにしたので、誰がどう使ってもOK(ただし無保証)。

XAMPPで htdocs 下に置いたファイルが他のコンピュータから見えなかった件

XAMPPは、便利です。これをひとつインストールするだけで、ApacheMySQLPHPFTPインストーラーでかんたんにインストールできます。
このあいだから、MacでもXAMPPを使ってWEBアプリをいじっていましたが、とくにさしつかえなく使えていました。

ところが今回、 XAMPP インストールフォルダ直下の htdocs (これは実際にはシンボリックリンクですが)に置いたWEBアプリを iPhone6 からアクセスしてみよーと思って試すと、失敗してしまいました。
HTTPエラー、403 (forbidden)。Apacheの承認が拒否されるのでした。

(IPアドレス)/xampp/

これでアクセスしてみたら403のエラーになります。

でも、ローカルPCで(つまりXAMPPを起動しているPCにあるブラウザで)XAMPPのアプリにアクセスする分には問題なくWEBページが表示できます。

localhost/xampp/

その表示されたページに記載された セキュリティ というリンクを開くと、

これらのXAMPPページは一般的にネットワーク経由でアクセス可能です

現在ご覧になっているすべてのXAMPPデモページは、ネットワーク上で一般的にアクセス可能です。あなたのIPアドレスを知っている人は誰でもこれらのページを見ることができます。

なんて書いてあるくらいです(いまはそれを見ることができなくて困っているのに)。

つまり、XAMPPを配布してくれている人たちは、XAMPPのインストールが正常に終わったときには、IPアドレスを知ってさえいればこのPCのウェブページを見ることができると考えているということですね。とくに手で設定ファイルを修正したりすることも必要なく。


気を取り直してXAMPP付属のWEBアプリのメニューから マニュアル へのリンクを開くと、 Apache 2 ドキュメント というのがあります。
そのリンクを開くと Apache HTTP サーバ バージョン 2.4 ドキュメント というページが表示されます。
その リリースノート のセクションには、 2.2 から 2.4 へのアップグレード というリンクがあります。
それを開くと(英語ですが)いろいろと「ここが変わったんだよ〜」的な説明があります。

「あれぇ? エラーだよ。なにが起きたんだか」ということで、Apache のエラーログを見ます。

XAMPPの Mac OS X 版は、Apache等の起動や停止に manager-osx というコントロールツールを使うようになっています。
そのツールの Manage Servers タブで Apache Web Server を選んでから Configure ボタンをクリックすると、 Configure Apache Web Server ダイアログが開くのですが、そこで Open Error Log をクリックすると Apache のエラーログが開きます。
エラーログには、

[authz_core:error]

とかで

AH01630: client denied by server onfiguration: /Applications/XAMPP/xamppfiles/htdocs/xampp/ 

というエラーメッセージが出ていました。

XAMPPのバージョンは XAMPP for Mac OS X 5.6.3-0 です。

エラーメッセージで検索すると、たいていの問題はだいたい片付くので、とりあえず Google 検索してみましたが、同じような問題がみつかったものの、その解決方法がどうもしっくりきません。おなじように対処しようとしてもそこに書かれているファイルがなかったり、試してみても同じ成功の結果がえられなかったり。

「そもそも前にもXAMPPは使っていたけど、 403 になんてならなかったのにな」という疑問がわきました。

その理由は、 Apache が 2.2 系から 2.4 系に変わったためらしいです。で、そのために変更になった Apache の設定があるというので真似してみましたが、設定ファイルを修正して Apache を再起動してからブラウザでアクセスしなおしてみても、同じ403エラーのままでした。

「おかしいなぁ。おかしいなぁ。」といったんは諦めて別のことをはじめてみたりもしました。

で、もういちど向きなおって、 Apache の設定ファイルがどこにあるのか、そのパスを探してみました。

Apache の設定ファイルは httpd.conf という名前のテキストファイルですが、 XAMPP for Mac OS X の場合はこのファイルは /Applications/XAMPP/etc/httpd.conf です。ところが、 etc が xamppfiles/etc へのシンボリックリンクになっているので、その実体は /Applications/XAMPP/xamppfiles/etc/httpd.conf というパスにあります。このファイルの末尾に、次のような記述がありました。

# XAMPP
Include etc/extra/httpd-xampp.conf
Include "/Appliactions/XAMPP/xamppfiles/apache2/conf/httpd.conf"

ここに記された /Appliactions/XAMPP/xamppfiles/apache2/conf/httpd.conf というファイルの内容は、数行のテキストファイルでした。

Alias /bitnami/ "/Applications/XAMPP/xamppfiles/apache2/htdocs/"
Alias /bitnami "/Applications/XAMPP/xamppfiles/apache2/htdocs"

<Directory "/Applications/XAMPP/xamppfiles/apache2/htdocs">
    Options Indexes FollowSymLinks
    AllowOverride All
    Order allow,deny
    Allow from all
</Directory>

これを見ると、 /bitnami というURL( http://localhost/bitnami )という別名(ブラウザで見るときの名前)が定義してあります。で、そのフォルダについての承認は誰に対しても開かれているようです。

でも、それは "/Applications/XAMPP/xamppfiles/apache2/htdocs" についてであって、
"/Applications/XAMPP/xamppfiles/htdocs" についてではないんですね。

ということなので、

(IPアドレス)/bitnami/

というURLを試してみました。

うまく表示されました。

/Applications/XAMPP/htdocs/xxx に置いたWEBページを http://(IPアドレス)/xxx で参照することはできませんでした。
でも
/Applications/XAMPP/xamppfiles/apache2/htdocs/xxx に置いたWEBページを http://(IPアドレス)/bitnami/xxx で参照することはできるということになりそうです。

設定ファイルは変えなくて、WEBアプリのディレクトリの起き場所を変えればよいわけですね。
(「もうちょっと説明がされていてもよかったんじゃないかなぁ」と思わされました。)

phonegapのインストールを試す

PhoneGap というのを使うと、iPhone のアプリを JavaScript と HTML とCSS で書けるというのでインストールしてみた。(同じソースで Android のアプリも作れてしまうらしい。それぞれにアプリの作法の違いもあるから同じに作れてもそれでは好ましくないということもあるだろうけど、できるということは便利なことだ。)

インストールにあたって npm を使うので、そのためにまず node.js をインストールする。
node.js はインストーラーがあるので、起動して、質問に答えていけば無事に終わった。

それから、phonegap自体を

sudo npm install -g phonegap

Windows の場合は sudo なし。 Mac の場合は root 権限で実行するために sudo ではじめることが必要。)

コマンドラインで実行すればいいらしかったけど、
ECONNRESET というエラーが起きて失敗した。

KIMURA-no-MacBook-Pro:~ kimurashinichi$ sudo npm install -g phonegap

WARNING: Improper use of the sudo command could lead to data loss
or the deletion of important system files. Please double-check your
typing when using sudo. Type "man sudo" for more information.

To proceed, enter your password, or type Ctrl-C to abort.

Password:
npm WARN deprecated deflate-crc32-stream@0.1.2: module has been merged into crc32-stream
npm ERR! Darwin 14.1.1
npm ERR! argv "node" "/usr/local/bin/npm" "install" "-g" "phonegap"
npm ERR! node v0.12.2
npm ERR! npm  v2.7.4
npm ERR! code ECONNRESET
npm ERR! errno ECONNRESET
npm ERR! syscall read

npm ERR! network read ECONNRESET
npm ERR! network This is most likely not a problem with npm itself
npm ERR! network and is related to network connectivity.
npm ERR! network In most cases you are behind a proxy or have bad network settings.
npm ERR! network 
npm ERR! network If you are behind a proxy, please make sure that the
npm ERR! network 'proxy' config is set properly.  See: 'npm help config'
npm ERR! Darwin 14.1.1
npm ERR! argv "node" "/usr/local/bin/npm" "install" "-g" "phonegap"
npm ERR! node v0.12.2
npm ERR! npm  v2.7.4
npm ERR! code ECONNRESET
npm ERR! errno ECONNRESET
npm ERR! syscall read

npm ERR! network read ECONNRESET
npm ERR! network This is most likely not a problem with npm itself
npm ERR! network and is related to network connectivity.
npm ERR! network In most cases you are behind a proxy or have bad network settings.
npm ERR! network 
npm ERR! network If you are behind a proxy, please make sure that the
npm ERR! network 'proxy' config is set properly.  See: 'npm help config'
npm ERR! Darwin 14.1.1
npm ERR! argv "node" "/usr/local/bin/npm" "install" "-g" "phonegap"
npm ERR! node v0.12.2
npm ERR! npm  v2.7.4
npm ERR! code ECONNRESET
npm ERR! errno ECONNRESET
npm ERR! syscall read

検索すると、プロキシを使ってるのにプロキシの設定がしてないと出る、なんて書いてあったりしたものの、結局は、 npm の registry の設定をするとうまくいった。

sudo npm -g config set registry http://registry.nodejs.org/

npm の設定に registry 項目が追加されてる。

KIMURA-no-MacBook-Pro:~ kimurashinichi$ sudo npm -g config list
; cli configs
global = true
user-agent = "npm/2.7.4 node/v0.12.2 darwin x64"

; globalconfig /usr/local/etc/npmrc
registry = "http://registry.npmjs.org/"

; node bin location = /usr/local/bin/node
; cwd = /Users/kimurashinichi
; HOME = /Users/kimurashinichi
; 'npm config ls -l' to show all defaults.

これでうまくいった。

KIMURA-no-MacBook-Pro:~ kimurashinichi$ sudo npm install -g phonegap
npm WARN engine npm@1.4.28: wanted: {"node":">=0.8","npm":"1"} (current: {"node":"0.12.2","npm":"2.7.4"})
npm WARN engine cordova-js@3.7.3: wanted: {"node":"~0.10.x"} (current: {"node":"0.12.2","npm":"2.7.4"})
npm WARN engine xmlbuilder@2.2.1: wanted: {"node":"0.8.x || 0.10.x"} (current: {"node":"0.12.2","npm":"2.7.4"})
npm WARN deprecated deflate-crc32-stream@0.1.2: module has been merged into crc32-stream
 
> ws@0.4.31 install /usr/local/lib/node_modules/phonegap/node_modules/connect-phonegap/node_modules/socket.io/node_modules/engine.io/node_modules/ws
> (node-gyp rebuild 2> builderror.log) || (exit 0)

  CXX(target) Release/obj.target/bufferutil/src/bufferutil.o

> ws@0.4.31 install /usr/local/lib/node_modules/phonegap/node_modules/connect-phonegap/node_modules/socket.io/node_modules/socket.io-client/node_modules/engine.io-client/node_modules/ws
> (node-gyp rebuild 2> builderror.log) || (exit 0)

  CXX(target) Release/obj.target/bufferutil/src/bufferutil.o
/usr/local/bin/phonegap -> /usr/local/lib/node_modules/phonegap/bin/phonegap.js
phonegap@4.2.0-0.25.0 /usr/local/lib/node_modules/phonegap
├── pluralize@0.0.4
├── colors@0.6.0-1
├── semver@1.1.0
├── minimist@0.1.0
├── qrcode-terminal@0.9.4
├── shelljs@0.1.4
├── phonegap-build@0.9.1 (colors@0.6.2, qrcode-terminal@0.8.0, optimist@0.3.7, shelljs@0.0.9, phonegap-build-api@0.3.3)
├── prompt@0.2.11 (revalidator@0.1.8, pkginfo@0.3.0, read@1.0.5, winston@0.6.2, utile@0.2.1)
├── cordova@4.2.0 (underscore@1.7.0, q@1.0.1, nopt@3.0.1, cordova-lib@4.2.0)
└── connect-phonegap@0.15.0 (home-dir@0.1.2, connect-inject@0.3.2, ip@0.3.1, ncp@0.6.0, findit@2.0.0, shelljs@0.2.6, request-progress@0.3.1, tar@0.1.19, http-proxy@1.8.1, node-static@0.7.0, gaze@0.4.3, archiver@0.10.1, localtunnel@1.3.0, useragent@2.0.8, request@2.33.0, connect@2.12.0, socket.io@1.0.4)
KIMURA-no-MacBook-Pro:~ kimurashinichi$ 

PhoneGap は Apache Cordova という名前のプロジェクトにもなっているそうだけど、なるほど、無事インストールが終わると、cordovaもインストールされるんだ。cordova フォルダができてるね。

noteのもくじ

 誰でも参加できるので、私もきむらしんいちのnoteを作りました。あなたもnoteに参加してみませんか?(フォローしてもらえるとうれしいです。)

noteの検索

 公式機能はまだありません(計画中だそうです)が、結城浩さんによる非公式のnoteの検索Googleカスタム検索)を使えば検索もできますよ。

「純情小曲集」より「旅上」(萩原朔太郎)

原文

ふらんすへ行きたしと思へども
ふらんすはあまりに遠し
せめては新しき背広をきて
きままなる旅にいでてみん。
汽車が山道をゆくとき
みづいろの窓によりかかりて
われひとりうれしきことをおもはむ
五月の朝のしののめ
うら若草のもえいづる心まかせに

翻訳

フランスに行きたい と思っても
フランスは あまりに遠い
せめて 新しいスーツを着て
きままな旅に 出かけてみようか。
汽車が山道を進む そのとき
水色の窓に寄りかかって
ひとりそっと うれしいことを考えよう
五月の朝の 夜明けの雲
若草が芽生えるような 心のままに

性格類型検査のカードをもらった

先日、研修を受けて性格類型検査のカードをもらいました。
ひとりひとり違うことがわかって、ちょっとおもしろい研修でした。

15枚の価値観のカードを順位づけする

 私の順位づけはこうです。
(1)個性の発揮
(2)多様性
(3)公共性/公益性
(4)他者への影響力
(5)美的追求

40枚の行動のカードを好き嫌いと得意不得意で4分割する

好きで得意:活用したい行動

 したい順にならべると私の場合はこうでした。
(THING)理解する
(IDEA)問題解決する
(IDEA)ディスプレイする
(IDEA)記述する
(IDEA)伝達する
(IDEA)提案する
(PEOPLE)聴く
(PEOPLE)援助する

嫌いで不得意:強化したい行動

 したくない順にならべると私の場合はこうでした。
(PEOPLE)管理・監督する
(DATA)管理する
(DATA)見積もる
(IDEA)計画する

 計画を立てて定期的に計画からのブレがないか注意するように気を付ける必要があることがうかがわれます。秩序とか完璧性についての価値観の順位を上げるとよいのかもしれません。