Visual StudioのWebテストの結果をブラウザで参照可能にする

Visual Studio2005から、Webテストという機能でブラウザの操作を記録し、それをもとにテストケースを作成する機能がある。
負荷テストと組みわせることができたり、データベースやファイルの内容をもとに繰り返しテストをすることができたりと何かと便利だ。


そんな機能の1つとして、各種ブラウザのリクエストヘッダを偽装してテストを実行する機能がある。
これを使うと、それぞれのブラウザにたいして、どんなリクエストを返してくるかをテストすることができる。


しかし、難点が1つある。
それは、実際の表示イメージをVisual Studioからでしか確認できないという点だ。
これでは、せっかくリクエストヘッダでブラウザの偽装をしても、実際の表示画面が確認できないので、片手落ちだ。


そこで、これを何とかしてみようといろいろと調べてみた。
まず、テストの実行結果は、TestResultディレクトリの中に、trxファイルとしてxml形式で格納されることがわかった。
しかし、このあとの情報がなかなかみつからない。


いろいろと試行錯誤してみた結果、以下のような形式になっているらしいことがわかった

  • RequestとResponseの概要は、「WebRequestResult」要素に格納されているらしい
  • 実際のResponseの内容は、ByteArrayCacheの中のEntry要素に格納されているらしい
  • WebRequestResultとEntryは、それぞれ、responseBytesHandle属性とhandle属性で関連づけされているらしい
  • Responseの内容はGZipで圧縮されたあとBase64エンコードされた状態で格納されている


以上の内容をもとに、つくってみた復元プログラム
(相対パスの変換は未対応)

追記:
この内容は、Visual Studio 2008の情報です。
Visual Studio2005のtrxファイルは、また別の形式っぽいです

using System;
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;
using System.IO;
using System.IO.Compression;
using System.Xml;

namespace Rei
{
    public class Converter
    {
        private Encoding defaultEncoding = Encoding.GetEncoding("Shift-JIS");

        private Regex srcRegex = new Regex("src=\"(?<url>[^\"]+)\"", RegexOptions.Compiled | RegexOptions.IgnoreCase);
        private Regex hrefRegex = new Regex("href=\"(?<url>[^\"]+)\"", RegexOptions.Compiled | RegexOptions.IgnoreCase);
        
        /// <summary>
        /// trxファイルの内容を解析し、レスポンスの内容をファイルに書き出す
        /// その際、リンク先のurlは書き出した内容に合わせて修正する
        /// </summary>
        /// <param name="inputFileName">trxファイルの場所</param>
        /// <param name="outputDirName">出力ファイルの置き場所</param>
        public void Convert(string inputFileName,string outputDirName)
        {
            XmlDocument xmldoc = new XmlDocument();
            xmldoc.Load(inputFileName);

            XmlNamespaceManager nsmgr = new XmlNamespaceManager(xmldoc.NameTable);
            nsmgr.AddNamespace("a", "http://microsoft.com/schemas/VisualStudio/TeamTest/2006");

            List<WebRequestResult> reqResults = new List<WebRequestResult>();

            foreach (XmlNode node in xmldoc.SelectNodes("//a:WebRequestResult",nsmgr))
            {
                string responseHandle = node.Attributes["responseBytesHandle"].Value;

                XmlNode childNode = node.SelectSingleNode("a:Request",nsmgr);
                string url = childNode.Attributes["url"].Value;

                WebRequestResult reqresult = new WebRequestResult();
                reqresult.ResponseHandle = responseHandle;
                reqresult.Url = url;

                XmlNode responseNode = node.SelectSingleNode("a:Response", nsmgr);
                reqresult.ContentType = responseNode.Attributes["contentType"].Value;

                reqResults.Add(reqresult);
            }

            foreach (WebRequestResult reqresult in reqResults)
            {
                string handle = reqresult.ResponseHandle;
                XmlNode entryNode = xmldoc.SelectSingleNode("//a:Entry[@handle =\"" + handle + "\"]",nsmgr);
                reqresult.EncodedResponse = entryNode.Attributes["bytes"].Value;
            }


            foreach (WebRequestResult reqresult in reqResults)
            {
                string outFileName = outputDirName + "\\" + reqresult.fileName;
                using(FileStream fs = new FileStream(outFileName,FileMode.Create))
                {
                    if (!reqresult.ContentType.StartsWith("text"))
                    //if (true)
                    {
                        byte[] bytes = reqresult.Response;
                        fs.Write(bytes, 0, bytes.Length);
                        fs.Close();
                    }
                    else
                    {
                        Encoding enc = null;
                        if (reqresult.Charset != string.Empty)
                        {
                            enc = Encoding.GetEncoding(reqresult.Charset);
                        }
                        else
                        {
                            enc = defaultEncoding;
                        }

                        string body = enc.GetString(reqresult.Response, 0, reqresult.Response.Length);
                        Dictionary<string, string> urlMap = new Dictionary<string, string>();

                        MatchCollection srcMatches = srcRegex.Matches(body);
                        foreach (Match match in srcMatches)
                        {
                            string src = match.Groups["url"].Value;
                            if (!urlMap.ContainsKey(src))
                            {
                                for (int i = 0; i < reqResults.Count; i++)
                                {
                                    if (reqResults[i].Url.EndsWith(src))
                                    {
                                        urlMap.Add(src, reqResults[i].fileName);
                                        break;
                                    }
                                }
                            }
                        }
                        MatchCollection hrefMatches = hrefRegex.Matches(body);
                        foreach (Match match in hrefMatches)
                        {
                            string src = match.Groups["url"].Value;
                            if (!urlMap.ContainsKey(src))
                            {
                                for (int i = 0; i < reqResults.Count; i++)
                                {
                                    if (reqResults[i].Url.EndsWith(src))
                                    {
                                        urlMap.Add(src, reqResults[i].fileName);
                                        break;
                                    }
                                }
                            }
                        }
                        foreach (string beforeUrl in urlMap.Keys)
                        {
                            Regex regex1 = new Regex("src=\"" + beforeUrl + "\"", RegexOptions.IgnoreCase);
                            Regex regex2 = new Regex("href=\"" + beforeUrl + "\"", RegexOptions.IgnoreCase);
                            body = regex1.Replace(body, "src=\"" + urlMap[beforeUrl] + "\"");
                            body = regex2.Replace(body, "href=\"" + urlMap[beforeUrl] + "\"");
                        }

                        byte[] converted = enc.GetBytes(body);
                        fs.Write(converted, 0, converted.Length);
                        fs.Close();

                    }
                }
            }
        }

        
    }

    internal class WebRequestResult
    {
        private static Regex charsetRegex = new Regex(@"charset=(?<charsetName>.+)", RegexOptions.Compiled);
        public string Charset
        {
            get {
                MatchCollection matches = charsetRegex.Matches(this.contentType);
                if (matches.Count > 0)
                {
                    return matches[0].Groups["charsetName"].Value;
                }
                return string.Empty; 
                }
        }

        private string contentType;
        public string ContentType
        {
            get { return this.contentType; }
            set {this.contentType = value;}
        }

        private string url;
        public string Url
        {
            get { return this.url; }
            set { this.url = value; }
        }

        private string guid = Guid.NewGuid().ToString();
        public string fileName
        {
            get
            {
                string name = Url;
                name = name.Replace("//", "_");
                name = name.Replace('/', '_');
                name = name.Replace(':', '_');
                name = name.Replace('*', '_');
                name = name.Replace('"', '_');
                name = name.Replace('<', '_');
                name = name.Replace('>', '_');
                name = name.Replace('|', '_');
                if (name.EndsWith("\\"))
                {
                    name = name.Substring(0, name.Length - 1);
                }
                if (name.Length > 100)
                {
                    return this.guid;
                }
                return name;
            }
        }

        private string responseHandle;
        public string ResponseHandle
        {
            get { return this.responseHandle; }
            set { this.responseHandle = value; }
        }

        private string encodedResponse;
        public string EncodedResponse
        {
            get { return this.encodedResponse; }
            set { this.encodedResponse = value; }
        }

        public byte[] Response
        {
            get {
                byte[] compressed = Convert.FromBase64String(this.encodedResponse);
                MemoryStream ms = new MemoryStream();
                ms.Write(compressed, 0, compressed.Length);
                ms.Position = 0;
                GZipStream gzipStream = new GZipStream(ms, CompressionMode.Decompress,true);

                MemoryStream msOut = new MemoryStream();
                int uncompressedByte = -1;
                while ((uncompressedByte = gzipStream.ReadByte()) != -1)
                {
                    msOut.WriteByte((byte)uncompressedByte);
                }
                return msOut.ToArray(); 
                }
        }
    }
}