refactor:将每个测试方法改为相互独立

原因是:

1.  相互独立的单元测试,有助于debug问题,
2.  不需要考虑执行顺序,额外的运行环境等,更加简单。
做的工作是:
1. 去掉了顺序执行,这有助于并发调用,减少测试时间
2. 去掉了测试方法中共享的实例
This commit is contained in:
zerlei 2024-07-30 15:36:42 +08:00
parent fc6c7bf8d2
commit 78d9e68fea
13 changed files with 265 additions and 166 deletions

View file

@ -6,4 +6,8 @@
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="SharpZipLib" Version="1.4.2" />
</ItemGroup>
</Project>

View file

@ -465,7 +465,7 @@ public class Dir(string path, List<AFileOrDir>? children = null, NextOpType? nex
{
var ldir = this;
var rdir = other;
Dir? cDir = new Dir(rdir.FormatedPath);
Dir? cDir = new(rdir.FormatedPath);
//分别对文件和文件夹分组
List<File> lFiles = [];
List<File> rFiles = [];

View file

@ -4,6 +4,7 @@ using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Security.AccessControl;
using System.Security.Principal;
using ICSharpCode.SharpZipLib.Zip;
using Microsoft.VisualBasic;
namespace Common;
@ -26,7 +27,8 @@ public abstract class FileDirOpStra
/// 文件目录打包
/// </summary>
/// <param name="dstRootPath"></param>
public class FileDirOpForPack(string srcRootPath, string dstRootPath) : FileDirOpStra
public class FileDirOpForPack(string srcRootPath, string dstRootPath, string syncId = "")
: FileDirOpStra
{
/// <summary>
/// 目标根目录
@ -38,12 +40,78 @@ public class FileDirOpForPack(string srcRootPath, string dstRootPath) : FileDirO
/// </summary>
public readonly string SrcRootPath = srcRootPath;
public readonly string SyncId = string.IsNullOrEmpty(syncId)
? Guid.NewGuid().ToString()
: syncId;
/// <summary>
/// 最终完成时的压缩
/// </summary>
public void FinallyCompress()
{
var x = DstRootPath;
static List<string> GetFilesResus(string dirPath)
{
var files = new List<string>();
foreach (var file in Directory.GetFiles(dirPath))
{
files.Add(file);
}
foreach (var dir in Directory.GetDirectories(dirPath))
{
foreach (var file in GetFilesResus(dir))
{
files.Add(file);
}
}
return files;
}
var fileNames = GetFilesResus(SrcRootPath);
var OuptPutFile = Path.GetDirectoryName(DstRootPath) + $"/{SyncId}.zip";
using FileStream fsOut = new(OuptPutFile, FileMode.Create);
using ZipOutputStream zipStream = new(fsOut);
{
zipStream.SetLevel(9); // 设置压缩级别
zipStream.Password = "VSXsdf.123d7802zw@#4_"; // 设置密码
byte[] buffer = new byte[4096];
foreach (string file in fileNames)
{
// Using GetFileName makes the result compatible with XP
// as the resulting path is not absolute.
var entry = new ZipEntry(file.Replace(SrcRootPath, ""));
// Setup the entry data as required.
// Crc and size are handled by the library for seakable streams
// so no need to do them here.
// Could also use the last write time or similar for the file.
//entry.DateTime = ;
entry.DateTime = System.IO.File.GetLastWriteTime(file);
zipStream.PutNextEntry(entry);
using (FileStream fs = System.IO.File.OpenRead(file))
{
// Using a fixed size buffer here makes no noticeable difference for output
// but keeps a lid on memory usage.
int sourceBytes;
do
{
sourceBytes = fs.Read(buffer, 0, buffer.Length);
zipStream.Write(buffer, 0, sourceBytes);
} while (sourceBytes > 0);
}
}
// Finish/Close arent needed strictly as the using statement does this automatically
// Finish is important to ensure trailing information for a Zip file is appended. Without this
// the created file would be invalid.
zipStream.Finish();
// Close is important to wrap things up and unlock the file.
zipStream.Close();
}
}
/// <summary>
@ -97,27 +165,70 @@ public class FileDirOpForPack(string srcRootPath, string dstRootPath) : FileDirO
this.FileCreate(absolutePath, mtime);
}
public override void FileDel(string absolutePath)
{
throw new NotImplementedException();
}
public override void FileDel(string absolutePath) { }
public override void DirDel(Dir dir, bool IsRecursion = true)
{
throw new NotImplementedException();
}
public override void DirDel(Dir dir, bool IsRecursion = true) { }
}
public class FileDirOpForUnpack(string srcCompressedPath, string dstRootPath) : FileDirOpStra
public class FileDirOpForUnpack(string srcRootPath, string dstRootPath, string syncId)
: FileDirOpStra
{
/// <summary>
/// 解压缩,必须首先调用
/// </summary>
public void FirstUnComparess()
{
var x = SrcCompressedPath;
string zipFilePath = $"{SrcRootPath}/{SyncId}.zip";
using (ZipInputStream s = new ZipInputStream(System.IO.File.OpenRead(zipFilePath)))
{
s.Password = "VSXsdf.123d7802zw@#4_";
ZipEntry theEntry;
while ((theEntry = s.GetNextEntry()) != null)
{
Console.WriteLine(theEntry.Name);
string directoryName =
DstRootPath + $"/{SyncId}/" + Path.GetDirectoryName(theEntry.Name)
?? throw new NullReferenceException("无法得到父文件目录!");
string fileName = Path.GetFileName(theEntry.Name);
// create directory
if (directoryName.Length > 0)
{
Directory.CreateDirectory(directoryName);
}
if (fileName != String.Empty)
{
using (
FileStream streamWriter = System.IO.File.Create(
directoryName + "/" + fileName
)
)
{
int size = 2048;
byte[] data = new byte[2048];
while (true)
{
size = s.Read(data, 0, data.Length);
if (size > 0)
{
streamWriter.Write(data, 0, size);
}
else
{
break;
}
}
}
}
}
}
}
public readonly string SyncId = syncId;
/// <summary>
/// 目标根目录
/// </summary>
@ -126,7 +237,7 @@ public class FileDirOpForUnpack(string srcCompressedPath, string dstRootPath) :
/// <summary>
/// 源目录
/// </summary>
public readonly string SrcCompressedPath = srcCompressedPath;
public readonly string SrcRootPath = srcRootPath;
/// <summary>
/// 最终完成时的压缩

View file

@ -0,0 +1,33 @@
using System.Text;
using Microsoft.AspNetCore.Mvc;
namespace LocalServer.Controllers
{
public class LocalServerController(LocalSyncServerFactory factory) : ControllerBase
{
private readonly LocalSyncServerFactory Factory = factory;
[Route("/")]
public async Task WebsocketConnection(string Name)
{
if (HttpContext.WebSockets.IsWebSocketRequest)
{
try
{
var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
Factory.CreateLocalSyncServer(webSocket, Name);
}
catch (Exception e)
{
HttpContext.Response.Body = new MemoryStream(Encoding.UTF8.GetBytes(e.Message));
HttpContext.Response.StatusCode = StatusCodes.Status406NotAcceptable;
}
}
else
{
HttpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
}
}
//TODO 是否在本地记载同步日志?
}
}

View file

@ -1,33 +0,0 @@
using Microsoft.AspNetCore.Mvc;
namespace LocalServer.Controllers
{
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
private readonly ILogger<WeatherForecastController> _logger;
public WeatherForecastController(ILogger<WeatherForecastController> logger)
{
_logger = logger;
}
[HttpGet(Name = "GetWeatherForecast")]
public IEnumerable<WeatherForecast> Get()
{
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
})
.ToArray();
}
}
}

View file

@ -0,0 +1,19 @@
using System.Net.WebSockets;
namespace LocalServer;
public class LocalSyncServer
{
public readonly WebSocket Socket;
public readonly string Name;
public readonly LocalSyncServerFactory Factory;
public LocalSyncServer(WebSocket socket, string name, LocalSyncServerFactory factory)
{
Socket = socket;
Name = name;
Factory = factory;
}
}

View file

@ -0,0 +1,22 @@
using System.Net.WebSockets;
namespace LocalServer;
public class LocalSyncServerFactory
{
public void CreateLocalSyncServer(WebSocket socket, string name)
{
if (Servers.Select(x => x.Name == name).Any())
{
throw new Exception("there already is a server with that name is Runing!");
}
Servers.Add(new LocalSyncServer(socket, name, this));
}
private readonly List<LocalSyncServer> Servers = [];
public void RemoveLocalSyncServer(LocalSyncServer server)
{
Servers.Remove(server);
}
}

View file

@ -1,34 +1,26 @@
using LocalServer;
var builder = WebApplication.CreateBuilder(args);
namespace LocalServer
// Add services to the container.
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddSingleton<LocalSyncServerFactory>();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseAuthorization();
app.MapControllers();
app.Run();
}
}
}
app.UseWebSockets();
app.UseAuthorization();
app.MapControllers();
app.Run();

View file

@ -1,13 +0,0 @@
namespace LocalServer
{
public class WeatherForecast
{
public DateOnly Date { get; set; }
public int TemperatureC { get; set; }
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
public string? Summary { get; set; }
}
}

View file

@ -1,31 +1,24 @@
using Common;
using Xunit;
/*using Newtonsoft.Json;*/
using XUnit.Project.Attributes;
namespace ServerTest;
/// <summary>
/// xUnit将会对每个测试方法创建一个测试上下文IClassFixture可以用来创建类中共享测试上下文
///
/// XUnit 的测试方法不是按照顺序执行,所以注意对象状态
///
/// 一般单元测试,每个测试函数应当是独立的,不让它们按照顺序执行,在一般情况下是最好的做法,参考
/// https://learn.microsoft.com/en-us/dotnet/core/testing/unit-testing-best-practices
/// 目前涉及到一些文件的同步所以按照顺序执行相对较好这使用了xUnit的方法使它们按照顺序执行
/// </summary>
///
[TestCaseOrderer(
ordererTypeName: "XUnit.Project.Orderers.PriorityOrderer",
ordererAssemblyName: "ServerTest"
)]
public class DirFileOpTest(FilesSeed filesSeed) : IClassFixture<FilesSeed>
public class DirFileOpTest : IDisposable
{
private readonly FilesSeed filesSeed = filesSeed;
private readonly FilesSeed filesSeed = new();
public void Dispose()
{
//filesSeed.Dispose();
GC.SuppressFinalize(this);
}
/// <summary>
/// 测试文件目录写入和提取
/// </summary>
[Fact, TestPriority(0)]
[Fact]
public void FileDirWriteExtract()
{
filesSeed.NewDir.WriteByThisInfo(filesSeed.fileDirOp);
@ -41,11 +34,10 @@ public class DirFileOpTest(FilesSeed filesSeed) : IClassFixture<FilesSeed>
/// <summary>
/// 测试文件差异比较
/// </summary>
[Fact, TestPriority(1)]
[Fact]
public void FileDirDiff()
{
var cDDir = filesSeed.NewDir.Diff(filesSeed.OldDir);
// Console.WriteLine("################################");
// Console.WriteLine(cDDir.Children.Count);
//Assert.True(IsSuccess);
@ -57,9 +49,10 @@ public class DirFileOpTest(FilesSeed filesSeed) : IClassFixture<FilesSeed>
/// <summary>
/// 测试同步是否成功
/// </summary>
[Fact, TestPriority(2)]
[Fact]
public void SyncFileDir()
{
filesSeed.OldDir.WriteByThisInfo(filesSeed.fileDirOp);
filesSeed.OldDir.CombineJustDirFile(filesSeed.fileDirOp, filesSeed.DiffDir);
Dir oldSync = new(filesSeed.OldDir.FormatedPath);
oldSync.ExtractInfo();
@ -70,7 +63,7 @@ public class DirFileOpTest(FilesSeed filesSeed) : IClassFixture<FilesSeed>
/// <summary>
/// 测试文件合并
/// </summary>
[Fact, TestPriority(3)]
[Fact]
public void DirsCombine()
{
filesSeed.OldDir.CombineJustObject(filesSeed.DiffDir);
@ -79,4 +72,19 @@ public class DirFileOpTest(FilesSeed filesSeed) : IClassFixture<FilesSeed>
// Console.WriteLine(filesSeed.OldDir.Path);
Assert.True(filesSeed.OldDir.IsEqual(filesSeed.NewDir), "合并结果不一致!");
}
[Fact]
public void Tt()
{
filesSeed.NewDir.WriteByThisInfo(filesSeed.fileDirOp);
var c = new FileDirOpForPack(filesSeed.NewDir.FormatedPath, filesSeed.TestPath + "/");
c.FinallyCompress();
var d = new FileDirOpForUnpack(
filesSeed.TestPath + "/",
filesSeed.TestPath + "/",
c.SyncId
);
d.FirstUnComparess();
}
}

View file

@ -176,16 +176,22 @@ public class FilesSeed : IDisposable
fileDirOp = new SimpleFileDirOp();
}
private readonly string TestPath = Path.Combine(Directory.GetCurrentDirectory(), "../../..");
public readonly string TestPath = Path.Combine(Directory.GetCurrentDirectory(), "../../..");
public Dir NewDir;
public Dir OldDir;
public Dir DiffDir;
public FileDirOpStra fileDirOp;
public void Dispose()
{
if (Directory.Exists($"{TestPath}/OldDir"))
{
Directory.Delete($"{TestPath}/OldDir", true);
}
if (Directory.Exists($"{TestPath}/NewDir"))
{
Directory.Delete($"{TestPath}/NewDir", true);
}
Console.WriteLine("FilesSeed Dispose");
GC.SuppressFinalize(this);
}

View file

@ -1,41 +0,0 @@
using Xunit.Abstractions;
using Xunit.Sdk;
using XUnit.Project.Attributes;
namespace XUnit.Project.Orderers;
public class PriorityOrderer : ITestCaseOrderer
{
public IEnumerable<TTestCase> OrderTestCases<TTestCase>(
IEnumerable<TTestCase> testCases) where TTestCase : ITestCase
{
string assemblyName = typeof(TestPriorityAttribute).AssemblyQualifiedName!;
var sortedMethods = new SortedDictionary<int, List<TTestCase>>();
foreach (TTestCase testCase in testCases)
{
int priority = testCase.TestMethod.Method
.GetCustomAttributes(assemblyName)
.FirstOrDefault()
?.GetNamedArgument<int>(nameof(TestPriorityAttribute.Priority)) ?? 0;
GetOrCreate(sortedMethods, priority).Add(testCase);
}
foreach (TTestCase testCase in
sortedMethods.Keys.SelectMany(
priority => sortedMethods[priority].OrderBy(
testCase => testCase.TestMethod.Method.Name)))
{
Console.WriteLine(testCase);
yield return testCase;
}
}
private static TValue GetOrCreate<TKey, TValue>(
IDictionary<TKey, TValue> dictionary, TKey key)
where TKey : struct
where TValue : new() =>
dictionary.TryGetValue(key, out TValue? result)
? result
: (dictionary[key] = new TValue());
}

View file

@ -1,9 +0,0 @@
namespace XUnit.Project.Attributes;
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class TestPriorityAttribute : Attribute
{
public int Priority { get; private set; }
public TestPriorityAttribute(int priority) => Priority = priority;
}