サポンテ 勉強ノート

サポンテの勉強ノート・読書メモなどを晒します。

TSV(Excel または他の表計算ソフト)から xUnit のテスト引数(InlineData)に変換するコードジェネレータ【C#】

はじめに

 xUnit で [Theory] を指定するテストメソッドでは [InlineData()] を使用して、複数のテストケースを記述できます。

 ですが引数が多くなってくると面倒で、楽をしたくなります。

 いつものごとく、コードジェネレータを作成します。

コードジェネレータ

 左の入力欄に、サンプルのように TSV 文字列をコピペして、convert ボタンをクリックしてください。右の入力欄に、生成した C# のコードが出力されます。

 サンプル文字列が既に入力されているので、convert ボタンをクリックするだけで動作を確認できます。

生成されるコード例

 以下のようなコードが出力されます。

[Theory()]
[InlineData(1, "Apple", 200)]
[InlineData(2, "Banana", 150)]
[InlineData(3, "Citrus", 300)]
public void TestMethod(int Id, string Name, int Price)

オプション

 オプションが二つあり、チェックボックスで有効/無効を切り替えます。

Use CamelCase

 チェックすると、メソッドの引数部分の識別子をキャメルケースにします。

Use String

 チェックすると、全ての引数を文字列としてコードを生成します。

ソースコード

 念のため、ソースコードも載っけておきます。JSFiddle がサービス終了しちゃうかもしれませんからね。

 それに、オフラインで使いたいという要件もあるかもしれません(その場合、CDN のところは修正してください)。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>TSV (from Excel or other) to xUnit InlineData Definition</title>
    <style>
        .outer {
            display: flex;
            justify-content: center;
            align-items: center;
        }
        #app {
            margin: 0 auto;
            display: inline-block;
            text-align: left;
            height: 95%;
        }
        textarea {
            font-family: monospace;
        }
        @media (prefers-color-scheme: dark) {
            body {
                background-color: #333;
                color: #fff;
            }
            textarea {
                background-color: #444;
                color: #fff;
            }
        }
    </style>
</head>
<body>
    <div class="outer">
        <div id="app">
            <table>
                <tr>
                    <td rowspan="3">
                        <textarea name="" id="src" cols="30" rows="10" v-model="src" placeholder="Excel などからコピペしてください"></textarea>
                    </td>
                    <td></td>
                    <td rowspan="3">
                        <textarea name="" id="result" cols="30" rows="10" v-model="result"></textarea>
                    </td>
                </tr>
                <tr>
                    <td>
                        <input type="checkbox" id="useCamelCase" v-model="useCamelCase" />
                        <label for="useCamenCase">Use CamelCase</label>
                        <br />
                        <input type="checkbox" id="useString" v-model="useString" />
                        <label for="useString">Use String</label>
                        <br />
                    </td>
                </tr>
                <tr>
                    <td>
                        <button v-on:click="convert">convert</button><br />
                        <button v-on:click="copyResult">copy</button>
                    </td>
                </tr>
            </table>
        </div>
    </div>

    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
    <script>
        var vm = new Vue({
            el: "#app",
            data: {
                src: "Id\tName\tPrice\n1\tApple\t200\n2\tBanana\t150\n3\tCitrus\t300",
                result: "",
                ln: "grid",
                useCamelCase: true,
                useString: false,
            },
            methods: {
                convert: function (e) {
                    let ret = '';
                    let lines = this.getLines();
                    const useCC = this.useCamelCase;
                    const useStr = this.useString;

                    // create header
                    ret = "[Theory()]\n";
                    lines.forEach((line, row) => {
                        if (row === 0) {return;}
                        var csv = [];
                        line.forEach((col, idx) => {
                            if (useStr || isNaN(col)) {
                                csv.push('"' + col + '"');
                            } else {
                                csv.push(col);
                            }
                            
                        });
                        ret += "[InlineData(" + csv.join(', ') + ")]\n";
                    });

                    // create types
                    let types = [];
                    const isInt = function (src) {
                        const n = Number.parseFloat(src);
                        return Number(n) === n && n % 1 === 0;
                    };
                    lines[1].forEach((col, idx) => {
                        if (!this.useString && !isNaN(col)) {
                            if (isInt(col)) {
                                types.push('int');
                            } else {
                                types.push('double');
                            }
                        } else {
                            types.push('string');
                        }
                    });

                    // create method 
                    ret += 'public void TestMethod(';
                    var params = [];
                    lines[0].forEach((col, idx) => {
                        if (useCC) {col = this.camelCase(col);}
                        params.push(types[idx] + ' ' + col);
                    });
                    ret += params.join(', ');
                    ret += ")\n";
                    this.result = ret;
                },
                getLines: function () {
                    var linesSrc = this.src.split("\n");
                    var lines = [];
                    linesSrc.forEach(line => {
                        if (line.trim() ==='') {return;}
                        lines.push(line.trim().split("\t"));
                    });
                    return lines;
                },
                camelCase: function (str){
                    // http://webdesign-dackel.com/2015/05/15/js-change-case/
                    str = String(str);
                    str = str.charAt(0).toLowerCase() + str.slice(1);
                    return str.replace(/[-_](.)/g, function(match, group1) {
                        return String(group1).toUpperCase();
                    });
                },
                copyResult: function () {
                    navigator.clipboard
                        .writeText(this.result)
                        .then(() => {
                            console.log('copied!')
                        })
                        .catch(e => {
                            console.error(e)
                        });
                }
            }
        });
    </script>
</body>
</html>

最後に

 2021-11-30 バグ修正。

 なんか Edge だとコピーができない。