在软件开发领域中工作的任何人都将需要在旧代码中添加功能,这些功能可能是从先前的团队继承而来的,您需要对其进行紧急修复。
可以在文献中找到许多遗留代码的定义,我更喜欢的定义是:“通过遗留代码,我们指的是我们害怕改变的有利可图的代码”。
该定义包含两个基本概念:
1.该代码必须有利可图。如果不是这样,我们将无意对其进行更改。2.它必须引起对修改它的恐惧,因为我们可以引入新的bug或依赖影子的东西。
在以下情况下,更容易出错:
•测试未涵盖该代码。•代码不干净;不遵守单一责任原则。•该代码的设计不正确,或者随着时间的流逝其结构变得不合理:对一段代码进行更改可能会产生一些副作用。•您没有时间全面了解正在修改的内容。
测试是我们作为开发人员可用的强大武器。这些测试为我们提供了结果的安全性,并且是一种快速检测错误的方法。但是,我们如何测试未知的代码?构建单元测试套件将为我们提供有关该项目的深入知识,但它将使长时间保持高成本。如果我们无法测试细节,则可以使用Characterization Test,它是描述软件行为的测试。
在这种情况下起重要作用的模式是“ 黄金大师模式”。基本思想很简单:如果我们无法深入了解,我们需要一些有关整个执行过程的指标。我们捕获正确执行的输出(stdout,图像,日志文件等),这就是我们的Golden Master,可用于预期输出。如果当前执行的输出匹配,我们可以确信我们的更改没有引入新的错误。
为了展示Golden Master Pattern的用法,让我们从一个示例开始(完整的代码可以在此处[1]找到)。我们公司开发了用于命令行的游戏,包括井字游戏(该游戏的实现从此处获取[2]),我们的老板要求我们更改游戏以提供调整游戏板尺寸的能力。让我们看一下代码:
namespace Tris
{
public class Game
{
//making array and
//by default I am providing 0-9 where no use of zero
static char[] arr = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };
static int player = 1; //By default player 1 is set
static int choice; //This holds the choice at which position user want to mark
// The flag variable checks who has won if its value is 1 then someone has won the match if -1 then Match has Draw if 0 then match is still running
static int flag = 0;
public static void run()
{
do
{
Console.Clear();// whenever loop will be again start then screen will be clear
Console.WriteLine("Player1:X and Player2:O");
Console.WriteLine("\n");
if (player % 2 == 0)//checking the chance of the player
{
Console.WriteLine("Player 2 Chance");
}
else
{
Console.WriteLine("Player 1 Chance");
}
Console.WriteLine("\n");
Board();// calling the board Function
choice = int.Parse(Console.ReadLine());//Taking users choice
// checking that position where user want to run is marked (with X or O) or not
if (arr[choice] != 'X' && arr[choice] != 'O')
{
if (player % 2 == 0) //if chance is of player 2 then mark O else mark X
{
arr[choice] = 'O';
player++;
}
else
{
arr[choice] = 'X';
player++;
}
}
else //If there is any position where user wants to run and that is already marked then show message and load board again
{
Console.WriteLine("Sorry the row {0} is already marked with {1}", choice, arr[choice]);
Console.WriteLine("\n");
Console.WriteLine("Please wait 2 second board is loading again.....");
Thread.Sleep(2000);
}
flag = CheckWin();// calling of check win
} while (flag != 1 && flag != -1);// This loof will be run until all cell of the grid is not marked with X and O or some player is not winner
Console.Clear();// clearing the console
Board();// getting filled board again
if (flag == 1)// if flag value is 1 then someone has win or means who played marked last time which has win
{
Console.WriteLine("Player {0} has won", (player % 2) + 1);
}
else// if flag value is -1 the match will be drawn and no one is the winner
{
Console.WriteLine("Draw");
}
Console.ReadLine();
}
// Board method which creats board
private static void Board()
{
Console.WriteLine(" | | ");
Console.WriteLine(" {0} | {1} | {2}", arr[1], arr[2], arr[3]);
Console.WriteLine("_____|_____|_____ ");
Console.WriteLine(" | | ");
Console.WriteLine(" {0} | {1} | {2}", arr[4], arr[5], arr[6]);
Console.WriteLine("_____|_____|_____ ");
Console.WriteLine(" | | ");
Console.WriteLine(" {0} | {1} | {2}", arr[7], arr[8], arr[9]);
Console.WriteLine(" | | ");
}
private static int CheckWin()
{
#region Horzontal Winning Condtion
//Winning Condition For First Row
if (arr[1] == arr[2] && arr[2] == arr[3])
{
return 1;
}
//Winning Condition For Second Row
else if (arr[4] == arr[5] && arr[5] == arr[6])
{
return 1;
}
//Winning Condition For Third Row
else if (arr[6] == arr[7] && arr[7] == arr[8])
{
return 1;
}
#endregion
#region vertical Winning Condtion
//Winning Condition For First Column
else if (arr[1] == arr[4] && arr[4] == arr[7])
{
return 1;
}
//Winning Condition For Second Column
else if (arr[2] == arr[5] && arr[5] == arr[8])
{
return 1;
}
//Winning Condition For Third Column
else if (arr[3] == arr[6] && arr[6] == arr[9])
{
return 1;
}
#endregion
#region Diagonal Winning Condition
else if (arr[1] == arr[5] && arr[5] == arr[9])
{
return 1;
}
else if (arr[3] == arr[5] && arr[5] == arr[7])
{
return 1;
}
#endregion
#region Checking For Draw
// If all the cells or values filled with X or O then any player has won the match
else if (arr[1] != '1' && arr[2] != '2' && arr[3] != '3' && arr[4] != '4' && arr[5] != '5' && arr[6] != '6' && arr[7] != '7' && arr[8] != '8' && arr[9] != '9')
{
return -1;
}
#endregion
else
{
return 0;
}
}
}
}
快速阅读后,代码看上去很混乱,职责没有正确分开,变量名也没有意义。
经过准确的阅读后,我们可以找到游戏板,该游戏板存储在“ static char [] arr”中。向阵列添加新元素没有任何效果,因为该阵列直接在PrintBoard和CheckWin函数中访问。现在我们知道要调整游戏板的大小,必须更改大部分代码。
创建一个新项目并运行游戏:
class Program
{
static void Main(string[] args)
{
Game.run();
}
}
一旦我们印刷了棋盘,游戏就会要求用户输入。我们可以通过从文件中读取输入来实现自动化。
class Program
{
private const string InputPath = "input.txt";
public static void Main(string[] args)
{
var input = new StreamReader(new FileStream(InputPath, FileMode.Open));
Console.SetIn(input);
Game.run();
input.Close();
}
}
所有输入的集合太大,无法使用蛮力测试。我们可以做的就是对输入进行采样。为此,我们考虑井字游戏的最终得分:
•玩家1获胜•玩家2获胜•绘制图形
选择覆盖这三种情况的最低限度的测试集,在文本文件中编写路径,并在golendenMaster文件夹中收集结果:
class Program
{
private const string InputFolderPath = "input/";
private const string OutputFolderPath = "goldenMaster/";
public static void Main(string[] args)
{
int i = 1;
foreach (var filePath in Directory.GetFiles(InputFolderPath)) {
var input = new StreamReader(new FileStream(filePath, FileMode.Open));
var output = new StreamWriter(new FileStream(OutputFolderPath + "output" + i.ToString() + ".txt" , FileMode.CreateNew));
Console.SetIn(input);
Console.SetOut(output);
Game.run();
input.Close();
output.Close();
i++;
}
}
}
这三个结果文件代表了我们的Golden Master,我们可以在此基础上进行一些特性测试:
[Test]
public void WinPlayerOne()
{
inputPath = InputFolderPath + "input1.txt";
outputPath = OutputFolderPath + "output.txt";
var goldenMasterOutput = GoldenMasterOutput + "output1.txt";
var input = new StreamReader(new FileStream(inputPath, FileMode.Open));
var output = new StreamWriter(new FileStream(outputPath, FileMode.CreateNew));
Console.SetIn(input);
Console.SetOut(output);
Game.run();
input.Close();
output.Close();
Assert.True(AreFileEquals(goldenMasterOutput, outputPath));
}
private bool AreFileEquals(string expectedPath, string actualPath)
{
byte[] bytes1 = Encoding.Convert(Encoding.ASCII, Encoding.ASCII, Encoding.ASCII.GetBytes(File.ReadAllText(expectedPath)));
byte[] bytes2 = Encoding.Convert(Encoding.ASCII, Encoding.ASCII, Encoding.ASCII.GetBytes(File.ReadAllText(actualPath)));
return bytes1.SequenceEqual(bytes2);
}
只要测试是绿色的,我们就可以重构而不必担心破坏某些东西。一种可能的结果可能是:
public static void run()
{
char[] board = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };
int actualPlayer = 1;
while (CheckWin(board) == 0)
{
PrintPlayerChoise(actualPlayer);
PrintBoard(board);
var choice = ReadPlayerChoise();
if (isBoardCellAlreadyTaken(board[choice]))
{
PrintCellIsAlreadyMarketMessage(board[choice], choice);
continue;
}
board[choice] = GetPlayerMarker(actualPlayer);
actualPlayer = UpdatePlayer(actualPlayer);
}
PrintResult(board, actualPlayer);
}
从这段代码中可以看出Board的概念及其职责。让我们尝试在新的Board类中提取行为。新Board应能够:
•印刷板•标记玩家的选择•检查是否有赢家
使用TDD(更多详情,请阅读这篇文章[3])制定一个可调整大小的Board(发现测试的完整代码在这里[4]和阶级的一个位置[5])。现在尝试将它们插入游戏中,并检查Golden Master是否保持绿色:
private const int Boardsize = 3;
public static void run()
{
Board board = new Board(Boardsize);
int actualPlayer = 1;
while (board.CheckWin() == -1)
{
PrintPlayerChoise(actualPlayer);
Console.WriteLine(board.Print());
var choice = ReadPlayerChoise();
if (!board.UpdateBoard(actualPlayer, choice))
{
PrintCellIsAlreadyMarketMessage(board.GetCellValue(choice), choice);
continue;
}
actualPlayer = UpdatePlayer(actualPlayer);
}
PrintResult(board, actualPlayer);
}
此时,我们可以恢复标准输入/标准输出并从用户那里读取电路板的尺寸:
class Program
{
public static void Main(string[] args)
{
Console.WriteLine("Insert Diagonal dimension of Board: ");
var boardSize = int.Parse(Console.ReadLine());
Game.run(boardSize);
}
}
如您所见,多亏了Golden Master Pattern,我们能够控制遗留代码并进行重构,而无需担心。但是,所有闪闪发光的东西都不是金子:在“噪声输出”的情况下使用Golden Master可能会很困难,“噪声输出”对于执行无用,但会随时间(例如时间戳记,线程名等)而变化。在这种情况下,我们可以过滤输出并仅考虑重要部分。
我希望它在您下次重写旧项目时对您有用:毕竟,我们担心我们的代码失去控制!
References
[1]
此处: https://github.com/ntonjeta/GoldenMasterExample[2]
此处获取: https://www.c-sharpcorner.com/UploadFile/75a48f/tic-tac-toe-game-in-C-Sharp/[3]
这篇文章: https://www.blexin.com/en-US/Article/Blog/TDD-the-whole-code-is-guilty-until-proven-innocent-38[4]
在这里: https://github.com/ntonjeta/GoldenMasterExample/blob/master/test/GoldenMasterExampleTest/BoardShould.cs[5]
位置: https://github.com/ntonjeta/GoldenMasterExample/blob/master/lib/Game/Board.cs