如何在创业公司中编写高质量代码
如何在初创公司开发应用程序时保持合理的质量控制水平
由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!
如何在初创公司开发应用程序时保持合理的质量控制水平
关于我
我目前正在开发一个房地产平台、一系列软件产品和面向企业的解决方案。一些重要的产品即将发布。
关于这篇文章
它解释了如何为自己的创业公司构建解决方案,如何进行测试,最后给出了一些 .net core/csharp 示例。
我为什么同情那些有“想法”却不会编程的人
如果他们知道创办一家初创公司需要投入多少资金和开发人员的时间,他们根本就不会尝试。有时候我会遇到一些“生意人”,他们有个想法,却想让我帮他们做所有的开发工作,而他们却只负责市场营销——哈哈。总之……
创业思维与企业发展相结合
在很多方面,这与人们对创业公司的固有印象截然相反。作为一名开发者,我想要的是可扩展、灵活且实用的产品。这里没有发布管理团队、日常运营人员、测试人员或产品经理。只有你一个人,开发着最终能够实现商业价值的软件。
我们往往认为企业软件开发的质量高于初创公司,毫无疑问,企业中确实有一些非常优秀的开发人员。但企业开发人员很少被允许按照自己想要的方式构建应用程序,我认为这是个错误。例如,在企业中,我看到以下两种方法:
- 没有单元测试。
- 过度关注单元测试。
在创业公司里,你拥有前所未有的自由,可以按照自己的想法开发软件。但如果你想生存下去,你开发的大部分产品都必须非常出色,并且具有可修复性。
启动代码应该做什么;
- 在集成环境中工作。
- 范围应该比较小——较小的应用程序和代码。
- 如有需要,可提供单元测试的可能性。
- 证明它有效。
- 便于未来的开发者上手。
- 应该具有足够的模块化程度,以便在不损坏产品的情况下完全更换。
毋庸置疑,单凭一己之力实现上述目标非常困难。这篇文章的大部分内容,都源于我反复试验、不断纠错的经验——摒弃了我在商业环境中学到的东西。
作为一名独立创业公司的开发者,如何才能对自己的代码库感到满意?以下是一些启发式方法。
- 使用源代码控制。
- 将所有数据库和数据存储库备份到其他位置。
- 重大开发完成后,务必进行完整的离线代码备份。
- 确保你确信某件事有效,而不是仅仅认为某件事有效(这曾是最大的问题)。
- 主要侧重于集成测试。
- 使用单元测试,更多地是为了演示某个功能是如何运作以及预期运作的。
- 真正的单元测试应该更多地用于计算方面。
单元测试的挑战
有很多方面——本文的目的并非一一赘述。但总而言之:
- 虚假的安全感。
- 编写单元测试非常耗时。简而言之,使用单元测试会带来很大的额外开销。
- 没有考虑到许多特殊情况。
- 维护成本较高。
你可能已经感觉到我对单元测试的评价不高。过去15个月里,我的看法基本没变。单元测试的重要性不及集成测试,但使用单元测试框架来实现集成测试仍然非常重要。
提升初创公司代码库的质量
我使用 NUnit、.Net Core 依赖注入(虽然我更喜欢 Ninject),并且编写了自己的实用辅助程序,让一切变得更简单一些。
提供特定功能或服务的小型图书馆
这一点至关重要。我的一些库功能非常强大,但它们的核心功能通常只有一两个。最近的例子包括:
- 参考资料库。
- 邮箱分析和报告应用程序。
- 一个安全库(用于管理令牌化、持久化、加密和解密)。### 尽可能使用接口编写代码 首先,我确保尽可能使用接口编写代码。以下是一个接口示例;
namespace IRReferral.Services
{
public interface IEmailReferrer
{
ActivationMessage activationMessage { get; set; }
ActivityEmailFromAccount activityEmailFromAccount { get; set; }
IAddReferrerToProgram addReferrerToProgram { get; set; }
ISendEmailMessage sendEmailMessage { get; set; }
Task Message(string EmailAddress, IEnumerable<typJoinProgramAdd> addedPrograms);
}
}
通过面向接口进行编码,不仅可以利用依赖注入和控制反转,而且实际上,我几乎不可能不使用具体的实现。面向接口进行编码还允许我们在单元测试时根据需要进行模拟。
DI Binder 类
我在 .NET Core 之前就用过这种方法,在 .NET Core 中也同样适用。在目标库中,我们创建一个名为 DI(依赖注入)的文件夹,以及一个名为 Binder 的类。在单元测试项目中,我们创建一个与库名称相匹配的文件夹,并在其中创建一个(或多个)使用 Binder 类的类。这违反了所有关于服务定位器反模式的规则——但谁在乎呢,这只是个创业项目?
完整的代码稍后奉上。
AmazingLibrary
> DI
> Binder.cs
return ServiceProvider
Test Library
>AmazingLibraryTests
>TestAmazingClass.cs
>[Setup] Initialise local ServiceProvider variable.
ETC
测试类(测试类?)
我建议所有使用 C# 编程的人都应该阅读单元测试的艺术。进行单元测试时,理解伪类的重要性固然重要,但更重要的是实现代码重用。通常,我们会选择使用一个基类,并在测试类中继承该基类。单元测试不应该是糟糕的。
初创公司测试的核心概念是什么?
- 许多测试更侧重于集成测试。
- 许多测试不会包含断言,因为它们不是单元测试。
- 利用有序测试按顺序运行流程。
- 考虑一下在 C# 中编写单元测试是否正确,以及编写数据集成测试是否正确。
- 测试更多的是为了证明某事物在实际环境中能够发挥作用。
- 尽可能多地使用配置。## 我项目中的测试代码示例 我不想解释得太详细,因为这更多的是关于原理而不是代码的具体功能。所以代码会比较多,但请尽量理解其中的概念。### appsettings.config 文件中的 JSON
"ActivityEmailFromAccount": {
"Activity": "ReferralJoiner",
"EmailAccount": "me@inforhino.co.uk",
"MailHost": "somewhere.somewhere.co.uk",
"Password": "wouldntyouliketoknow",
"Port": 10000
}
实用辅助函数
这样,您就可以在测试项目中引用 appsettings.json 文件,并将其映射到要注入到依赖注入类中的对象。
static public class GetConfigurationItem
{
public static TItem GetConfigItem<TItem>()
{
var jsonSettingsFile =
$"{System.Reflection.Assembly.GetEntryAssembly().Location.Replace(System.Reflection.Assembly.GetEntryAssembly().ManifestModule.Name, "")}appsettings.json";
var builder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json");
var config = builder.Build();
var data = config.GetSection(typeof(TItem).Name)
.Get<TItem>();
return data;
}
}
DI Binder 类
这样可以解决依赖关系。你不一定要用它,但它很有帮助。
public class Binder
{
public ServiceProvider GetServiceProvider(ISetting setting
, ActivationMessage activationMessage
, ActivityEmailFromAccount activityEmailFromAccount)
{
ServiceProvider serviceProvider;
var services = new ServiceCollection();
services.AddScoped<ISetting>(x=>setting);
services.AddScoped<ActivationMessage>(x => activationMessage);
services.AddTransient<ActivityEmailFromAccount>(x => activityEmailFromAccount);
#region "Data manager"
services.AddTransient<IDataRepositoryManager, DataRepositoryManager>();
services.AddTransient(typeof(IClassMapper<,>), typeof(ClassMapper<,>));
services.AddTransient<IParameterBuilder, ParameterBuilder>();
services.AddTransient<Dictionary<string,ParameterFactory>>(
x=>new ReferralSetting().GetParameters());
services.AddTransient<ParameterAdder>();
services.AddTransient<ParameterFactory>();
services.AddTransient<string>();
#endregion
//Helpers
services.AddTransient<IMapProgramIDsTotypJoinProgramAdd, MapProgramIDsTotypJoinProgramAdd>();
services.AddTransient<IMapTextTotypTransformItemToSecure, MapTextTotypTransformItemToSecure>();
//repositories
services.AddTransient<IConfirmJoiningProgram, ConfirmJoiningProgram>();
services.AddTransient<IGetActivePrograms, GetActivePrograms>();
services.AddTransient<IGetProgramDetail, GetProgramDetail>();
services.AddTransient<IJoinProgram, JoinProgram>();
services.AddTransient<IGetUserTokensFromActivationToken, GetUserTokensFromActivationToken>();
//Services
services.AddTransient<IAddReferrerToProgram, AddReferrerToProgram>();
services.AddTransient<IEmailReferrer, EmailReferrer>();
services.AddTransient<IGetProgramSchemeDetail, GetProgramSchemeDetail>();
services.AddTransient<IManageReferrer, ManageReferrer>();
services.AddTransient<IVerifyProgramJoining, VerifyProgramJoining>();
services.AddTransient<ICurrentProgram, CurrentProgram>();
//External dependencies
services.AddTransient<IProcedure, Procedure>();
services.AddTransient<IVerifyProgramJoining, VerifyProgramJoining>();
services.AddTransient<IEncryption, Encryption>();
services.AddTransient<ITokeniser, Tokeniser>();
services.AddTransient<IHasher, Hasher>();
services.AddTransient<IMailSender, MailSender>();
services.AddTransient<ISendEmailMessage, SendEmailMessage>();
services.AddTransient<IVerifyProgramJoining, VerifyProgramJoining>();
serviceProvider = services.BuildServiceProvider();
return serviceProvider;
}
}
测试代码
这一点很重要。我们会根据测试的运行时间和与测试目标的交互方式对测试进行分类。我们会明确区分单元测试和集成测试。进行集成测试时,我们知道需要一些数据来验证预期结果——但随着公司的发展,我们可以利用这些数据来辅助完成这项工作。
基类
namespace IRTest
{
public class ReferralSettingFromConfig
{
public ActivationMessage activationMessage { get; set; } = GetConfigurationItem.GetConfigItem<ActivationMessage>();
public ISetting setting { get; set; }
= GetConfigurationItem.GetConfigItem<ReferralSetting>();
public ActivityEmailFromAccount activityEmailFromAccount { get; set; } =
GetConfigurationItem.GetConfigItem<ActivityEmailFromAccount>();
}
public class ReferralMailMessageRetriever
{
public ReferralSettingFromConfig referralSettingFromConfig { get; set; }
= new ReferralSettingFromConfig();
}
}
测试类本身
有一点显而易见,那就是或许应该把一些常用函数移到其他地方。我们不希望测试类变得过于庞大。
关键在于,我的代码编写方式能够体现这一点;
- 事物运作原理。
- 仍有改进空间。
- 表现出对编写相对简洁代码的尊重。
[TestFixture]
public class DataTests : ReferralMailMessageRetriever
{
private ServiceProvider serviceProvider { get; set; }
private string fullURL = @"http://www.inforhino.co.uk/ActivateReferral&token=610038006100620063006500360300300031003400620036003700380064003900someremoved00320037003600360035003200610030003300390032003700";
private string testEmail = "zak_willis@somewhere.com";
[SetUp]
public void SetUp()
{
var referralSetting = referralSettingFromConfig;
this.serviceProvider = new IRReferral.DI.Binder().GetServiceProvider(
referralSetting.setting
, referralSetting.activationMessage
, referralSetting.activityEmailFromAccount
);
}
[TearDown]
public void TearDown()
{
}
[Order(0)]
[Category("Integration Test")]
[TestCase("Product Recommendations,Services Referrals")]
public void TestGettingProgramDetail(string programName)
{
var programDetailRetriever = this.serviceProvider.GetRequiredService<IGetProgramDetail>();
List<typProgramSearch> programSearch =
programName.Split(",").Select(x => new typProgramSearch()
{ CurrentTMS = DateTime.Now, ProgramName = x }).ToList();
var data = programDetailRetriever.Get(programSearch);
}
[Order(0)]
[Category("Integration Test")]
[Test]
public void TestGettingActiveProgram()
{
var programs = GetCurrentPrograms();
}
public IEnumerable<typProgram> GetCurrentPrograms()
{
var activePrograms = this.serviceProvider.GetRequiredService<ICurrentProgram>();
var data = activePrograms.Retrieve(DateTime.Now);
return data;
}
public GetProgramDetail_data GetProgramSchemeDetail(IEnumerable<typProgramSearch> activePrograms)
{
var programData = this.serviceProvider.GetRequiredService<IGetProgramSchemeDetail>();
var data = programData.Retrieve(activePrograms);
return data;
}
public async Task AddReferrerAndEmailThem(string ReferrerEmail, IEnumerable<int> JoinedPrograms)
{
var manageReferrer = this.serviceProvider.GetRequiredService<IManageReferrer>();
await manageReferrer.Process(ReferrerEmail, JoinedPrograms);
}
[Order(1)]
[Category("Integration Test")]
[Test]
public async Task TestAddingReferrerAndEmailThem()
{
string ReferrerEmail = this.testEmail;
var programs = GetCurrentPrograms().Take(2).Select(x => x.ProgramId).ToList();
await AddReferrerAndEmailThem(ReferrerEmail, programs);
}
public byte[] HexStringFromURL()
{
string hexToken = fullURL.Split("=")[1];
var dataFromHex = Hex.GetCorrectEncodedArrayFromText(hexToken);
return dataFromHex;
}
[Category("Unit Test")]
[Test]
public void TestGettingUrlToToken()
{
var tokenFromHex = HexStringFromURL();
Assert.Less(0, tokenFromHex.Length);
}
[Category("Integration Test")]
[Test]
public void TestGettingTokenFromDB()
{
var tokenRetriver = this.serviceProvider.GetRequiredService<IGetUserTokensFromActivationToken>();
var tokenFromHex = HexStringFromURL();
tokenRetriver.Get(tokenFromHex, DateTimeOffset.Now);
Assert.Less(0, tokenFromHex.Length);
}
}
关于初创公司编程的结论
毫无疑问,我有时会看到一些测试代码,然后想:这到底是什么鬼?比如,为什么用逗号分割字符串而不是用数组?不过这并不重要。我们更关心的是如何测试框架的各个部分。
本文尚未完成,但希望它能展现出在创业公司中,为了在不花费太多时间编写测试的情况下保证产品质量,需要付出巨大的努力。
从这次经验中我们得到的启示是,在将功能移至更高级别的代码(例如网站或批量处理框架)之前,应该先找到某种方法来测试其功能。
文章来源:https://dev.to/zakwillis/how-to-try-and-build-quality-code-in-a-startup-214d使用StackEdit编写。