How to maintain a sensible level of quality control when building applications in a startup
About me
Am currently working on a property platform, a series of software products, and business focused solutions. Getting close to releasing some significant products.
About this post
It explains building solutions for your own startup, how to test and finishes with some .net core/csharp examples.
Why I feel for people with "ideas" who can't code
If they knew, just how much money and developer's time it will take to create a startup, they would not even try to do a startup. Sometimes I get "business" people who have an idea, and want to do the marketing whilst I do all the development for them - lol. Anyway....
Startup mentality, mixed with Enterprise development
In many ways, it is the opposite of what the narrative is on startups. As a developer, I want extensible, flexible, working products. There aren't any release management teams, business as usual staff, testers, product managers. It is just you, creating software that should become commercially viable.
We tend to think that Enterprise software development is higher quality than startups, and no doubt, there are some incredible developers in enterprises. Enterprise developers are rarely allowed to build applications the way they would want to, and to my mind this is a mistake. For example, in enterprises, I see these two approaches;
- No unit testing.
- An over-focus on unit testing.
In a startup - you have more freedom than ever to build software how you want. If you want to survive - most of what you build has to be really good and be salvageable.
What startup code should do;
- Work in an integration environment.
- Should be fairly concise in scope - smaller applications and code.
- Offer the potential to be unit tested if required.
- Demonstrate it works.
- Be relatively straightforward for future developers to pick up.
- Should be modular enough to be completely replaced without harming the product.
Suffice to say, achieving the above, single-handed is very hard to achieve. Most of what this post is about, has come more from trial and error - rejecting what I have learned in a commercial setting.
Heuristics for being comfortable with your codebase as a developer on a lone startup
- Use Source Control.
- Backup any databases, data repositories elsewhere.
- Take full offline code backups when major development has been done.
- Make sure you know something works rather than thinking something works (this has been the biggest problem).
- Focus mainly on Integration Testing.
- Use unit tests, more as a demonstration on how something works and was intended to work.
- Use true unit tests more for calculations only.
Challenges with unit testing
Are many - going into them all is not the purpose of this post. To summarise though;
- False sense of security.
- Takes a long time to write. Quite simply, there is a big overhead in using unit tests.
- Doesn't take account of the many edge cases.
- Has a maintenance burden.
You will probably sense that I don't have a very high opinion of unit testing. I would say the course of this 15 months - I feel pretty much the same as before. Unit Tests take second place to Integration Tests but using Unit Test Frameworks to achieve this is very important.
Adding quality to startup codebases
I use NUnit, .Net Core Dependency Injection (although I prefer Ninject) and write my own utility helpers to make everything a bit easier.
Small Libraries doing specific functions or services
This is paramount. Some of my libraries does quite a lot - but, they only center around a one or two primary functions. Recent examples includes;
- A referrals library.
- A Mailbox analyser and reporting application.
- A Security Library (for managing tokenisation, persistence, encryption and decryption. ### Code to interfaces as much as possible First of all, I make sure I code to interfaces as much as possible. Here is an example interface;
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);
}
}
By coding to interfaces, not only can you take advantage of Dependency Injection and Inversion of Control. In reality, how likely am I to not use one concrete implementation? Not very. Coding to interfaces allows us to fake if we need to when unit testing.
The DI Binder class
I used this approach pre .Net Core and it makes sense in .Net Core too. Inside the target library, we create a folder called DI (Dependency Injection) and a class called Binder. Within a unit test project, we create a folder appropriately named to the library and a class (or more) using that Binder class. This breaks all rules about service locator anti-patterns - but who cares, it's a startup?
Full code will be later.
AmazingLibrary
> DI
> Binder.cs
return ServiceProvider
Test Library
>AmazingLibraryTests
>TestAmazingClass.cs
>[Setup] Initialise local ServiceProvider variable.
ETC
The Test class (test classes?)
I recommend anybody coding in C# reads the art of unit testing. When unit testing, it is important to understand the importance of fakes, but more importantly implement code reuse. Often, we may decide to use a base class and inherit that base class in the test class. Unit tests shouldn't be crap.
Core concepts for testing in startups?
- Many tests are more geared towards integration tests.
- Many tests won't contain asserts because they are not unit tests.
- Make use of ordered tests to run processes in order.
- Consider whether writing unit tests inside C# is correct versus writing data integration tests.
- Testing is more to do with proving something works in an environmental capacity.
- Use configuration as much as possible. ## Code sample of testing in my projects I don't want to make this too explanatory as it is more to do with the principles than what the code does. So there will be a lot of code, but try to take the concepts. ### JSON inside appsettings.config file
"ActivityEmailFromAccount": {
"Activity": "ReferralJoiner",
"EmailAccount": "me@inforhino.co.uk",
"MailHost": "somewhere.somewhere.co.uk",
"Password": "wouldntyouliketoknow",
"Port": 10000
}
Utility Helper Function
This lets you refer to your appsettings.json file inside your test project and map it to an object for injecting into your Dependency Injection class.
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;
}
}
The DI Binder class
This resolves dependencies. You don't have to use it, but it helps.
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;
}
}
Test Code
This is important to appreciate. We try to categorise our tests in terms of when they run, and how they are interacting with our test target. We clarify whether a test is a Unit Test versus an Integration Test. When using an integration Test, we know there needs to be some data to see what we expect - but again, once our company grows people can help with that.
The base class
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();
}
}
The Test Class itself
One thing which immediately stands out, is, perhaps some of these common functions may get moved elsewhere. We don't want test classes getting large.
The critical thing is, I code it in a way which shows;
- How something works.
- Could be improved.
- Shows some respect towards making code relatively clean.
[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);
}
}
Conclusion on coding for startups
Undoubtedly, I look at some of the test code and say, what the hell is that? i.e. why split a string using a comma and not an array? It doesn't matter. We care more about having a way to test parts of our framework.
This article is not complete, but hopefully it shows the heavy commitment needed to try and get quality within a startup without spending too much time writing tests.
What has come from this experience is trying to have some way to test functionality before moving it to higher level code such as a website or processing batch framework.
Written with StackEdit.
Top comments (10)
Write integration tests for the most crucial happy paths of your users.
Nicely written and the code looks neat. But won't people eventually write something bad in the long run ?
Ha ha... Good point...
I shouldn't have mentioned quality code. I think it is more - things we know works without spending loads of time testing it. It isn't to do with people writing bad stuff, we can't care about that - it happens. It is more that we try to keep code open for deeper testing in the future if needed.
Cheers for reading.
Do not spent too much time in assuring quality during development of an MVP.
Time should be invested getting new clients and iterating to improve the product.
I deliberately chose not to approach it in the mvp way of where 999 out of 1000 startups fail and instead have enough salvageable artefacts at the end. This is more of a survival approach but thanks for recommending.
Some comments may only be visible to logged-in visitors. Sign in to view all comments.