使用流对类进行单元测试的最佳方法?

时间:2018-12-11 05:02:36

标签: c# unit-testing stream

我目前正在编写一些代码,以尝试对工作中的存储策略的两个部分进行分离和抽象。当前,我们使用JSON格式存储到文件中,然后将其检索为持久存储。我正在尝试将两个概念分开:

1)概念一将序列化与存储类型分开

2)概念二将存储类型与序列化策略分开。

我找到了一种对各种线程进行一些研究的好方法,例如使用TextWriter / TextReader而不是直接使用Files,以便可以使用任何Stream类型(FileStream / MemoryStream / etc),从而可以进行单元测试。完成没有文件。但是,我遇到了一个问题,因为包装流的TextWriter / TextReader类在流本身被处理后会自动关闭并处理,这是我在实践中想要的,但是却使我陷入了单元测试的麻烦。

这是我到目前为止拥有的代码...这是针对概念1(序列化过程)的。这是它的接口:

/// <summary>
/// Interface for a serializer which reads from a stream and creates a type
/// </summary>
public interface IInSerializer
{
    /// <summary>
    /// Load type from a stream
    /// </summary>
    /// <param name="reader"></param>
    /// <returns></returns>
    bool Load(TextReader reader);
}

/// <summary>
/// Interface for writing a type out into a stream
/// </summary>
public interface IOutSerializer
{
    /// <summary>
    /// Save to the stream
    /// </summary>
    /// <param name="writer"></param>
    /// <returns></returns>
    bool Save(TextWriter writer);
}

/// <summary>
/// Helper interface which provides interface <see cref="IInSerializer"/> 
/// and <see cref="IOutSerializer"/> for both reading/writing
/// </summary>
public interface IInOutSerializer : IInSerializer, IOutSerializer
{
}

这是JSON格式的序列化程序的抽象实现:

/// <summary>
/// Implementation of <see cref="IInOutSerializer"/> which serializes into JSON format
/// </summary>
/// <typeparam name="T">Type to be serialized</typeparam>
public abstract class JSONSerializer<T> : IInOutSerializer
{
    /// <summary>
    /// Source of serialization
    /// </summary>
    public T Source { get; set; }

    /// <summary>
    /// Provided by very specific type to load the Jobject into type T
    /// </summary>
    /// <param name="jObject"></param>
    /// <returns></returns>
    protected abstract bool LoadJObject(JObject jObject);
    /// <summary>
    /// Provided by very specific type to save type T into a Jobject
    /// </summary>
    /// <param name="jObject"></param>
    /// <returns></returns>
    protected abstract bool Serialize(JObject jObject);

    /// <summary>
    /// <see cref="IInOutSerializer.Load"/>
    /// </summary>
    /// <param name="reader"></param>
    /// <returns></returns>
    public bool Load(TextReader reader)
    {
        using (var json = new JsonTextReader(reader))
        {
            var jObject = JToken.ReadFrom(json) as JObject;
            if (jObject != null)
                return LoadJObject(jObject);
        }
        return false;
    }

    /// <summary>
    /// <see cref="IInOutSerializer.Save"/>
    /// </summary>
    /// <param name="writer"></param>
    /// <returns></returns>
    public bool Save(TextWriter writer)
    {
        var jObject = new JObject();
        if (Serialize(jObject))
        {
            using (var json = new JsonTextWriter(writer))
            {
                json.Formatting = Formatting.Indented;
                jObject.WriteTo(json);
                return true;
            }
        }
        return false;
    }
}

这是序列化我的Metro MetroDetails类的具体类型之一:

public class MetroLineJSONSerializationStrategy : JSONSerializer<MetroLineDetails>
{
    private class MetroLineHelper : IMetroLine, IMetroLineWritable
    {
        public string DestinationStation
        {
            get;
            set;
        }

        public Color LineColor
        {
            get;
            set;
        }

        public char LineLetter
        {
            get;
            set;
        }

        public string Name
        {
            get;
            set;
        }

        public bool SaturdayService
        {
            get;
            set;
        }

        public string SourceStation
        {
            get;
            set;
        }

        public bool SundayHolidayService
        {
            get;
            set;
        }

        public static explicit operator MetroLineDetails(MetroLineHelper source)
        {
            return new MetroLineDetails(source.Name, source.LineColor, source.SourceStation, source.DestinationStation, source.SaturdayService, source.SundayHolidayService);
        }
    }
    protected override bool LoadJObject(JObject jObject)
    {
        var helper = new MetroLineHelper();
        jObject.Read(nameof(MetroLineDetails.Name), (t) => (string)t, (v) => helper.Name = v);
        jObject.Read(nameof(MetroLineDetails.LineLetter), (t) => (char)t, (v) => helper.LineLetter = v);
        jObject.Read(nameof(MetroLineDetails.SourceStation), (t) => (string)t, (v) => helper.SourceStation = v);
        jObject.Read(nameof(MetroLineDetails.DestinationStation), (t) => (string)t, (v) => helper.DestinationStation = v);
        jObject.Read(nameof(MetroLineDetails.SaturdayService), (t) => (bool)t, (v) => helper.SaturdayService = v);
        jObject.Read(nameof(MetroLineDetails.SundayHolidayService), (t) => (bool)t, (v) => helper.SundayHolidayService = v);

        var color = jObject.Read(nameof(MetroLineDetails.LineColor), (t) => (JObject)t);
        helper.LineColor = color.ToColor();

        Source = (MetroLineDetails)helper;

        return true;
    }

    protected override bool Serialize(JObject jObject)
    {
        jObject.Add(nameof(MetroLineDetails.Name), Source.Name);
        jObject.Add(nameof(MetroLineDetails.LineLetter), Source.LineLetter);
        jObject.Add(nameof(MetroLineDetails.SourceStation), Source.SourceStation);
        jObject.Add(nameof(MetroLineDetails.DestinationStation), Source.DestinationStation);
        jObject.Add(nameof(MetroLineDetails.SaturdayService), Source.SaturdayService);
        jObject.Add(nameof(MetroLineDetails.SundayHolidayService), Source.SundayHolidayService);
        jObject.Add(nameof(MetroLineDetails.LineColor), Source.LineColor.ToJObject());

        return true;
    }
}

现在这是我的存储类型接口:

/// <summary>
/// Interface for the storage medium
/// </summary>
public interface IStorageMedium
{
    /// <summary>
    /// Save the information in the serializer
    /// </summary>
    /// <param name="serializer"></param>
    void Save(IOutSerializer serializer);
    /// <summary>
    /// Load the information to the serializer
    /// </summary>
    /// <param name="serializer"></param>
    void Load(IInSerializer serializer);
}

以及专门用于文件的类型:

/// <summary>
/// Implementation of <see cref="IStorageMedium"/> which stores into a file
/// </summary>
public class FileStorageMedium : IStorageMedium
{
    private readonly string _fileName;

    public FileStorageMedium(string fileName)
    {
        _fileName = fileName;
    }

    public void Save(IOutSerializer serializer)
    {
        using (var stream = new FileStream(_fileName, FileMode.Truncate))
        {
            using (var writer = new StreamWriter(stream))
            {
                serializer.Save(writer);
            }
        }
    }

    public void Load(IInSerializer serializer)
    {
        using (var stream = new FileStream(_fileName, FileMode.Open))
        {
            using (var reader = new StreamReader(stream))
            {
                serializer.Load(reader);
            }
        }
    }
}

正如您在每层中看到的那样,我想遵循最佳实践,并确保每种方法都为调用者关闭和刷新流,并且为了进行单元测试而不将其打开(我知道我可以将代码更改为没有关闭流,但是我认为那不合适)。

因此,现在,使用在论坛上发现的想法没有与文件流相关的任何东西来帮助进行单元测试,我仍然遇到寻找最佳单元测试方法的问题。这是我要编写的单元测试:

[TestClass]
public class MetroLine
{
    [TestMethod]
    public void TestSerialize()
    {
        var serializer = new MetroLineJSONSerializationStrategy();
        serializer.Source = new MetroLineDetails("A", Colors.Blue, "LA Union Station", "San Bernardino", true, true);
        using (var stream = new MemoryStream())
        {
            var writer = new StreamWriter(stream);
            serializer.Save(writer);
            stream.Seek(0, SeekOrigin.Begin);
            using (var reader = new StreamReader(stream))
            {
                var text = reader.ReadToEnd();
            }
        }
    }
}

无论我在serializer.Save()调用中做什么,流都是关闭的,因为该方法使用了一个可丢弃的流来关闭流(因为我认为这样做可以防止泄漏)。问题是,我无法再以任何方式对流进行单元测试以测试其中任何一项是否有效。我抛出异常,说您不能再访问封闭的流,这是有道理的。但是如何以任何有意义的方式测试流的内容?

1 个答案:

答案 0 :(得分:0)

我在MemoryStream上发现了GetBuffer,它使我可以将原始缓冲区转换为字符串,但是我可以对实际的JSON blob进行单元测试,这就是我写的:

    [TestMethod]
    public void TestSerialize()
    {
        var serializer = new MetroLineJSONSerializationStrategy();
        serializer.Source = new MetroLineDetails("Inland Empire Line", Colors.Blue, 'A', "LA Union Station", "San Bernardino", true, true);
        using (var stream = new MemoryStream())
        {
            using (var writer = new StreamWriter(stream))
            {
                serializer.Save(writer);
            }
            var bytes = stream.GetBuffer();
            var json = System.Text.Encoding.UTF8.GetString(bytes);
            Assert.AreEqual('{', json[0]);
        }
    }

我希望有人会发现它有用!