ささいなことですが。

Windowsアプリテスト自動化ライブラリFriendly開発者の日記です。

dotnetConf関西 2016 で登壇してきました。

これですね。
.Netの新しい機能の勉強会です。
connpass.com

僕はWPFとUWPのテスト自動化に関して話をしてきました。
まあ、WPFは新技術でもないですけど・・・。
とは言え、いい感じに枯れてきたので、日本でもようやく導入が始まってきたようですね。(過去何回も、日本でもようやく・・・的な話を聞きましたが、今回は本当のようですw)

WPFはFriendlyでバッチリテストできます。
UWPも結構いいところまで来てるんですよねー。
これも早いところAPI固めて正式版にしたい。

www.slideshare.net

デモが多かったのですが、
こちらから、そのアプリとテストをダウンロードできますので
よろしければ試してみてください。
github.com

Friendly.UWP_α 0.0.5 をリリースしました。

すごい久しぶりのリリースです。半年以上ほったらかしてましたからねー。
www.nuget.org

そもそも、動かなくなってた

おいおいって感じですが。UWPの画面描画に関するオブジェクトの生成をメインスレッド以外で実行した場合、例外が発生するようになっていました。Microsoftも結構な破壊的変更をぶっこんできますねー。newもメインスレッドで実行するようにしました。

UWPAppFriendから現在のウィンドウを取得できるプロパティを追加

地味機能です。さらにそこからContentも取得できます。もちろんFriendlyの素の機能でもできますが、インテリセンス効いた方が格段に便利ですよね。

var content = app.CurrentWindow.Content;

VisualTree検索機能

WPFの時に評判の良かったツリーからの検索機能を実装しました。UWPはLogicalTreeってないんですねー。VisualTreeからタイプとバインディングで検索できるようにしています。

var tree = app.CurrentWindow.Content.VisualTree();

//型から取得
var textBox = tree.ByType("Windows.UI.Xaml.Controls.TextBox").Single();

//バインディングパスから取得
var button = tree.ByBinding("Execute").Single());

コントロールラッパーの作成開始

とりあえず、以下作りました。順次増やしていきます。

  • Button
  • CheckBox
  • RadioButton
  • ComoboBox
  • ListBox
  • TextBox

で、今のところこんな操作ができます。

using Microsoft.VisualStudio.TestTools.UnitTesting;
using Codeer.Friendly.Dynamic;
using System.IO;
using VSHTC.Friendly.PinInterface;
using EnvDTE80;

namespace Friendly.UWP.Test
{
    [TestClass]
    public class TestAttach
    {
        [TestMethod]
        public void Test()
        {
            using (var app = new UWPAppFriend(new ByVisualStudio(Path.GetFullPath("../../../TargetApp/TargetApp.sln"))
            {
                VisualStudioPath = @"C:\Program Files (x86)\Microsoft Visual Studio 14.0\Common7\IDE\devenv.exe",
                ChangeVisualStudioSetting = (vs, dteSrc)=>
                {
                    var dte = dteSrc.Pin<DTE2>();
                    dte.Solution.SolutionBuild.SolutionConfigurations.Item(3).Activate();
                }
            }))
            {
                //Visual Tree取得
                var tree = app.CurrentWindow.Content.VisualTree();

                //テキストボックス
                var textBox = new TextBox(tree.ByType(TextBox.TypeFullName).Single());
                textBox.EmulateChangeText("abc");

                //ボタン
                var button = new Button(tree.ByBinding("Execute").Single());
                button.EmulateClick();

                //コンボボックス
                var comboBox = new ComboBox(tree.ByType(ComboBox.TypeFullName).Single());
                comboBox.EmulateChangeSelectedIndex(2);

                //リストボックス
                var listBox = new ListBox(tree.ByType(ListBox.TypeFullName).Single());
                listBox.EmulateChangeSelectedIndex(2);

                //ラジオボタン
                var radioButton = new RadioButton(tree.ByType(RadioButton.TypeFullName).Single());
                radioButton.EmulateCheck(true);

                //チェックボタン
                var check = new CheckBox(tree.ByType(CheckBox.TypeFullName).Single());
                check.EmulateCheck(true);

                //もちろん素のFriendlyの機能で、内部APIを直で実行できます。
                //背景色変更
                var mainPage = app.CurrentWindow.Content.Dynamic().Content;
                var color = app.Type("Windows.UI.Colors").Blue;
                var brush = app.Type("Windows.UI.Xaml.Media.SolidColorBrush")(color);
                mainPage.Content.Background = brush;
            }
        }
    }
}

実行結果です。
f:id:ishikawa-tatsuya:20160905010130p:plain

LambdicSqlがβ版になりました!

www.nuget.org

やっとβ版になりました。
1.0に向けて仕様は、ほぼ固まったかなー。これもおださんをはじめ、色々助言いただいた方々のおかげです。ありがとうございます!ちなみに織田さんには少し前からNugetのOwerにも入ってもらってます。

SQLをLambdaから作成するライブラリです

その名のとおりです。Linqとはアプローチが違って、書いたC#が(割と)そのままSQLになります。

public void TestStandard()
{
    var min = 3000;

    //C#でラムダを書くと
    var query = Db<DB>.Sql(db =>
        Select(new SelectData()
        {
            Name = db.tbl_staff.name,
            PaymentDate = db.tbl_remuneration.payment_date,
            Money = db.tbl_remuneration.money,
        }).
        From(db.tbl_remuneration).
            Join(db.tbl_staff, db.tbl_staff.id == db.tbl_remuneration.staff_id).
        Where(min < db.tbl_remuneration.money && db.tbl_remuneration.money < 4000));

    //文字列とパラメータになる!
    //デバッグ出力してみます。
    var info = query.Build(_connection.GetType());
    Debug.Print(info.SqlText);

    //後は、それを投げるだけ
    //Dapper使ってたらこんな感じです。
    var datas = _connection.Query(query).ToList();
}
SELECT
    tbl_staff.name AS Name,
    tbl_remuneration.payment_date AS PaymentDate,
    tbl_remuneration.money AS Money
FROM tbl_remuneration
    JOIN tbl_staff ON (tbl_staff.id) = (tbl_remuneration.staff_id)
WHERE ((@min) < (tbl_remuneration.money)) AND ((tbl_remuneration.money) < (@p_1))

シンプルなものだけじゃなくて、こんな複雑なものも書けます。

できること

こちらに一覧を書きました。(長いので転記はあきらめました。参照お願いします。)
github.com

多くの句、キーワード、関数、Window関数に対応

私が最近買った、SQLの入門書に書かれているDCLは全て対応しましたw。この本に載っているDCLはすべて書ける!(はず)CreateとかDropのようなDDLはサポートしていません。
「SQL ゼロからはじめるデータベース操作」
f:id:ishikawa-tatsuya:20160902233751p:plain

主要DBで動作

以下で動作確認しています。とは言え、同じ書き方でOKってわけでなくて、普通のSQLと同様に関数や句はDBごとに使える、使えないはあります。

DataBase type Support
SQL Server
SQLite
PostgreSQL
Oracle
MySQL
DB2

その他、便利機能が盛りだくさん

便利機能が盛りだくさんです。記事はα版の時に書いたものをβ版の仕様で修正していますので、参照お願いします。Dapper、EntityFrameworkの操作運用に関してはさらに便利にしたのでまた書きます。

早く1.0になりたい

残件は

  1. 関数コメント(英語)
  2. テスト追加
  3. チューニング
  4. リファクタリング

①が一番難易度高い...
9月中リリースを目標に進めよっと。

SQLWorld★大阪#38 に参加してきました。

SQL力を高めるべく、SqlWorld :: SQLWorld★大阪#38に参加してきました!
簡単なものから高度すぎるものまで、レンジの広い問題が出題されました。
で、その中でこんな問題がありました。
友達の人数を抽出するというものです。
f:id:ishikawa-tatsuya:20160825000048p:plain
で、私はこんなSQLを書きました。

SELECT 人.名前,
       isnull(友達人数.人数, 0) AS 人数
FROM   人
       LEFT OUTER JOIN
       (SELECT   友達一次元.ID AS ID,
                 sum(人数) AS 人数
        FROM     (SELECT   人ID1 AS ID,
                           count(*) AS 人数
                  FROM     友達
                  GROUP BY 人ID1
                  UNION
                  SELECT   人ID2 AS ID,
                           count(*) AS 人数
                  FROM     友達
                  GROUP BY 人ID2) AS 友達一次元
        GROUP BY 友達一次元.ID) AS 友達人数
       ON 人.ID = 友達人数.ID;

LambdicSqlで書いてみます

まあ、サブクエリの入れ子とか複雑ですよね。これをLambdicSqlで書いてみようと思います。(実はもっと良い書き方を主催のおださんから教えていただいたのですが、LambdicSqlの題材としてはこっちの方がよかったので)
LambdicSqlは順に組み立てられるのでこんなとき便利です。
最後のToSqlInfoでがSQL文字列が作成されます。

//テーブルの定義
class 人
{
    public int ID { get; set; }
    public string 名前 { get; set; }
}
class 友達
{
    public int 人ID1 { get; set; }
    public int 人ID2 { get; set; }
}
class DB
{
    public 人 人 { get; set; }
    public 友達 友達 { get; set; }
}

[TestMethod]
public void TestSqlWorld()
{
    //①友達テーブルの人ID1の列で友達の数を数えるクエリ
    var 人数1 = Sql<DB>.Create(db =>
        Select(new
        {
            ID = db.友達.人ID1,
            人数 = Count<int>(new Asterisk())
        }).
        From(db.友達).
        GroupBy(db.友達.人ID1));

    //②友達テーブルの人ID2の列で友達の数を数えるクエリ
    var 人数2 = Sql<DB>.Create(db =>
        Select(new
        {
            ID = db.友達.人ID2,
            人数 = Count<int>(new Asterisk())
        }).
        From(db.友達).
        GroupBy(db.友達.人ID2));

    //③合算して一列のテーブルにするクエリ
    var 友達一次元 = 人数1.Union(人数2);

    //④IDでグルーピングして人数を数える
    var 友達人数 = Sql<DB>.Create(db =>
    Select(new
    {
        ID = 友達一次元.Body.ID,
        人数 = Sum(友達一次元.Body.人数)
    }).
    From(友達一次元).
    GroupBy(友達一次元.Body.ID));

    //⑤クエリ完成
    //名前を付けるクエリ
    var 友達人数_名前付き = Sql<DB>.Create(db =>
        Select(new
        {
            名前 = db.人.名前,
            人数 = IsNull(友達人数.Body.人数, 0)
        }).
        From(db.人).
        LeftJoin(友達人数, db.人.ID == 友達人数.Body.ID)
    );

    //文字列とパラメータの取得
    //後はDapperへ
    var info = 友達人数_名前付き.ToSqlInfo(typeof(SqlConnection));
    Debug.Print(info.SqlText);
}

こんなSQLになります。

SELECT
	人.名前 AS 名前,
	ISNULL(友達人数.人数, @p_1) AS 人数
FROM 人
	LEFT JOIN 
	(SELECT
		友達一次元.ID AS ID,
		SUM(友達一次元.人数) AS 人数
	FROM 
		(SELECT
			友達.人ID1 AS ID,
			COUNT(*) AS 人数
		FROM 友達
		GROUP BY 友達.人ID1
		UNION
		SELECT
			友達.人ID2 AS ID,
			COUNT(*) AS 人数
		FROM 友達
		GROUP BY 友達.人ID2) 友達一次元
	GROUP BY 友達一次元.ID) 友達人数 ON (人.ID) = (友達人数.ID)

履歴

2016/09/02 β版対応

LambdicSql -主要DBゆるふわ対応-

LambdicSql_α0.0.63をリリースしました。β版間近です!
www.nuget.org

マルチDB対応

なんと主要6DBで動作確認しています。Surfaceに全部インストールしましたが、意外と入るものですね。(SQLiteはインストール不要)

DataBase type 動作確認
SQL Server
SQLite
PostgreSQL
Oracle
MySQL
DB2

とは言え、同じ書き方でOKなわけではないです。

この辺がゆるふわ。LambdicSqlは基本はそのままSQLのテキストになります。つまり、普通にSQL書く時と同じように使える句や関数だけ使うという方針です。

public void TestWindow()
{
    //SQLiteとMySqlはWindow関数使えないよ。
    if (_connection.GetType().FullName == "System.Data.SQLite.SQLiteConnection") return;
    if (_connection.GetType().FullName == "MySql.Data.MySqlClient.MySqlConnection") return;

    //make sql.
    var query = Db<DB>.Sql(db =>
        Select(new SelectData()
        {
            Avg = Window.Avg(db.tbl_remuneration.money).
                    Over<decimal>(new PartitionBy(db.tbl_staff.name, db.tbl_remuneration.payment_date),
                        new OrderBy(new Asc(db.tbl_remuneration.money), new Desc(db.tbl_remuneration.payment_date)),
                        new Rows(1, 5)),
            PaymentDate = db.tbl_remuneration.payment_date,
            Money = db.tbl_remuneration.money,
        }).
        From(db.tbl_remuneration).
        Join(db.tbl_staff, db.tbl_remuneration.staff_id == db.tbl_staff.id));

    //to string and params.
    var info = query.Build(_connection.GetType());
    Debug.Print(info.SqlText);

    //dapper
    var datas = _connection.Query<SelectData>(info.SqlText, info.Params).ToList();
}

オプションで、以下を切り替えれるようにしました。

じゃあ、何がマルチDB対応なんだよってことなのですが、以下の二つだけオプションで切り替えれるようにしています。こればっかりは書き分けられないので・・・。

  • 文字列の+演算
  • パラメータのプレフィックス
[TestMethod]
public void Test()
{
    var query = Db<DB>.Sql(db =>
        Select(new
        {
            Name = db.tbl_staff.name + "★",
            Money = db.tbl_remuneration.money,
        }).
        From(db.tbl_remuneration).
            Join(db.tbl_staff, db.tbl_staff.id == db.tbl_remuneration.staff_id).
        Where(1000000 < db.tbl_remuneration.money));

    //文字列化するときにオプションを指定する
    var info = query.Build(new SqlConvertOption() { ParameterPrefix = ":", StringAddOperator = "||" });
    Debug.Print(info.SqlText);
}
SELECT
	/*文字列結合の演算子に||を指定*/
	(tbl_staff.name) || (:p_0) AS Name,
	tbl_remuneration.money AS Money
FROM tbl_remuneration
	JOIN tbl_staff ON (tbl_staff.id) = (tbl_remuneration.staff_id)
/*オラクルは@使えない*/
WHERE (:p_1) < (tbl_remuneration.money)

なんで、こんなとこに差だすかなー。オラクルで@使ったらダメとか、わからなくて結構悩みました。

DataBase type 文字列結合 パラメータプレフィックス
SQL Server + @
SQLite || @
PostgreSQL || @
Oracle || :
MySQL + @
DB2 || @

これを毎回指定するのは面倒なんで、コネクション(SqlConnectionとか)のタイプを渡すと最適なオプションを選択するようにしています。

//コネクションタイプを渡すと、最適なオプションを選択します。
var info = query.ToSqlInfo(connection.GetType());

同一の記法でSQLテキストをカスタマイズする方法も提供しています。

長くなるので端折りますが、ToSqlInfoの第三引数にテキスト出力をカスタマイズするインターフェイスを渡せるようにしています。どうしてもやりたい人はこちらでできるように。また句や関数を自分で増やせる手段も提供しています。(拡張オレオレ句も作れる)

//第三引数でカスタマイズインターフェイスを渡せる仕様にしてます。
public static SqlInfo ToSqlInfo(this ISqlExpression exp, SqlConvertOption option, ISqlSyntaxCustomizer customizer)

LambdicSql - EntityFrameworkと仲良くなりました。

EntityFrameworkを使っているときでも、たまにSQLを直に書きたくなる場合がありますよね。そんな時はLambdicSqlを使うと便利です。そして、EntityFrameworkを使っている環境からLambdicSqlを使いやすい工夫を入れました。しかもそれでいてLambdicSql自体はDLL的にEntityFrameworkに依存したりはしていません。

DataContextクラスを指定できるようにしました。

EntityFrameworkでDBからモデルクラスを生成すると、こんな感じのものが生成されます。(若干省略してます)

public partial class tbl_remuneration
{
    public int? staff_id { get; set; }
    public string payment_date { get; set; }
    public int id { get; set; }
    public decimal? money { get; set; }
}

public partial class tbl_staff
{
    public int id { get; set; }
    public string name { get; set; }
}

public partial class ModelLambdicSqlTestDB : DbContext
{
    public virtual DbSet<tbl_remuneration> tbl_remuneration { get; set; }
    public virtual DbSet<tbl_staff> tbl_staff { get; set; }
}

これはLambdicSqlの定義に似ています。これをそのまま使えるようにしました。

public void TestEFAndLambdic()
{
    var query = Db<DB>.Sql(db =>
        Select(new
        {
            name = db.tbl_staff.T().name,
            payment_date = db.tbl_remuneration.T().payment_date,
            money = db.tbl_remuneration.T().money,
        }).
        From(db.tbl_remuneration.T()).
            Join(db.tbl_staff, db.tbl_remuneration.T().staff_id == db.tbl_staff.T().id);

    var datas = _connection.Query<SelectData1>(query).ToList();
}

T()がポイントですね。DbSet<>をスルーするものです。次のようなSQLになります。

SELECT
	tbl_staff.name AS "name",
	tbl_remuneration.payment_date AS "payment_date",
	tbl_remuneration.money AS "money"
FROM  tbl_remuneration
	JOIN tbl_staff ON (tbl_remuneration.staff_id) = (tbl_staff.id)

上記の例では素直にEFで書けばよいのですが、例えばOR結合を条件によって組み立てるときとか、Window関数を使うときとか、EFでは書きづらいもので利用してもらえばよいと思います。

//ORを任意の条件で組み立てるときとか
var exp = Sql<DB>.Create(db =>
    Condition(minCondition, 3000 < db.tbl_remuneration.T().money) ||
    Condition(maxCondition, db.tbl_remuneration.T().money < 4000));
var query1 = Sql<DB>.Create(db => SelectFrom(db.tbl_remuneration.T()).Where(exp));

//Window関数を使いたいときとか
var query2 = Db<DB>.Sql(db =>
    Select(new SelectData()
    {
        Average = Window.Avg(db.tbl_remuneration.T().money).
                Over<decimal>(null,
                    new OrderBy(new Asc(db.tbl_remuneration.T().payment_date)),
                    null)
    }).
    From(db.tbl_remuneration.T()));

名前解決のルールは若干違うので気を付けてください。

LambdicSqlのルールは、こちらです。EntityFrameworkとはテーブル名のルールが違っていますね。EFの場合はクラス名とテーブル名を対応させるルールで、しかも複数形と単数形の解決とか面倒なことをやってますね。とは言え、普通に生成するとEFも変数名とテーブル名は一致します。TableAttributeで指定している場合はLambdicSqlも同じルールで解決するので問題ないですね。問題ある命名している場合は、すみませんがテーブル定義を並べるクラスだけ別途作成お願いします。

是非EntityFrameworkユーザーの方もご利用お願いします!

LambdicSqlはメインでも使えて、脇役に回ってもいい仕事をするライブラリを目指しております。

LambdicSql - TableAttributeとColumnAttributeに対応しました。

LambdicSql_α0.0.53をリリースしました。
www.nuget.org

テーブル名とカラム名のルール

LambdicSqlのテーブル名とカラム名は、通常は変数名で表します。

public class Staff
{
    //変数名がカラム名になる
    public int id { get; set; }
    public string name { get; set; }
}
public class Remuneration
{
    public int id { get; set; }
    public int staff_id { get; set; }
    public DateTime payment_date { get; set; }
    public decimal money { get; set; }
}
public class DB
{
    //テーブル名も変数名で表す
    public Staff tbl_staff { get; set; }
    public Remuneration tbl_remuneration { get; set; }
}

スキーマを使う場合も変数名で表します。(もちろんスキーマを書く必要がない場合はこれは省略できます)

public class DBO
{
    public Staff tbl_staff { get; set; }
    public Remuneration tbl_remuneration { get; set; }
}
public class DB_EX
{
    //スキーマも変数名で表す
    public DBO dbo { get; set;}
}
//スキーマまで書きたい場合
var query = Sql<DB_EX>.Create(db =>
    Select(new
    {
        Name = db.dbo.tbl_staff.name,
        PaymentDate = db.dbo.tbl_remuneration.payment_date,
        Money = db.dbo.tbl_remuneration.money,
    }).
    From(db.dbo.tbl_remuneration).
        Join(db.dbo.tbl_staff, db.dbo.tbl_staff.id == db.dbo.tbl_remuneration.staff_id));

var info = query.ToSqlInfo(_connection.GetType());
Debug.Print(info.SqlText);
SELECT
	dbo.tbl_staff.name AS "Name",
	dbo.tbl_remuneration.payment_date AS "PaymentDate",
	dbo.tbl_remuneration.money AS "Money"
FROM dbo.tbl_remuneration
	JOIN dbo.tbl_staff ON (dbo.tbl_staff.id) = (dbo.tbl_remuneration.staff_id)

TableAttributeとColumnAttributeに対応しました。

それに加えてTableAttribute、ColumnAttributeでも名前を指定できるようにしました。
TableAttribute、ColumnAttributeは System.ComponentModel.DataAnnotations.dll に定義されている標準のものです。
テーブル名とか変数名をラムダ中で短く書きたい場合や、スキーマを一段クラスで表現するのが面倒な場合、それからテーブルやカラムの名前がC#の変数名で使えないものだったりする場合に利用できると思います。

//テーブル名をクラス定義時に指定できる
[Table("tbl_staff")]
public class StaffX
{
    //属性の方が優先される
    [Column("id")]
    public int idx { get; set; }
    [Column("name")]
    public string namex { get; set; }
}
public class DB
{
    //StaffXは属性でテーブル名を持っているのでそちらが優先される
    public StaffX xxx { get; set; }
    public Remuneration tbl_remuneration { get; set; }
}
var query = Db<DB>.Sql(db =>
    Select(new
    {
        name = db.xxx.namex,
        payment_date = db.tbl_remuneration.payment_date,
        money = db.tbl_remuneration.money,
    }).
    From(db.tbl_remuneration).
        Join(db.xxx, db.tbl_remuneration.staff_id == db.xxx.idx);

var info = query.Build(_connection.GetType());
Debug.Print(info.SqlText);
SELECT
	tbl_staff.name AS "name",
	tbl_remuneration.payment_date AS "payment_date",
	tbl_remuneration.money AS "money"
FROM tbl_remuneration
	JOIN tbl_staff ON (tbl_remuneration.staff_id) = (tbl_staff.id)