Can't even complain that it's a neologism and kids these days with their bad grammar; "yfelnyss" would appear to predate the Normans.
Nor is "evil" in the noun sense a great synonym for "evilness" in the noun sense. "Evilness" denotes the state of being bad, while "evil" denotes the badness itself. Evilness is the correct word here. Using "evil" would have been, if we want to be really pedantic (I hope I can safely assume that we do), been mildly nonsensical.
Good point! I can hardly complain of modern abuses of the language, given what the Saxons did to it. I await the seemingly inevitable "evilnessness".
[EDIT:] However, I can't agree that TFA is about "the state" of feature branching, or really even that feature branching is an entity that can possess a state. According to TFA, feature branching is bad. That's an intrinsic property, not one that feature branching will be able to overcome with appropriate prayer and reflection.
This is usually the case when abstract nouns are considered evil, so "evilness" is usually poor diction.
I'm afraid that this is simply a hypercorrection by analogy to color and such.
Indeed, we could not say "the black of feature branches"; it would have to be "blackness". Something that is black (adjective-wise) doesn't "have black" (the noun); it "has blackness".
The word "evil" doesn't conform to this restriction; "the evil of feature branches" is fine. Therefore "evilness" is just superfluousnessitude.
The word "bad" is color-like. We need "the badness of feature branches", not "the bad of feature branches".
But "evil" functions in such a way that it can substitute for "badness", and therefore for "evilness".
When we say "the evil of feature branches", the grammar is very similar to "the hypocrisy of using feature branches" or "the iniquity of using feature branches" or "the stink of feature branches" and such.
None of these words require an additional suffix; "stink" need not be worked into "stinkiness".
You exhibit a more complete command of English idiom than seen elsewhere in this sub-thread. One suspects that many of the people making these sorts of mechanical arguments haven't actually done much reading in English.
"evil" and "evilness" are not interchangeably usable, especially if you talk about the evil/evilness of X. The evil of X (or more frequently, evils of X) refers to the evil things that have been caused by X. This does not speak to whether or not X itself is evil, except insofar as doing evil is a sign of evilness. By contrast, the evilness of X is directly referring to the degree to which X is or is not evil (although were X not evil, you'd be more likely to talk about the goodness rather than evilness of X).
For an analogous example, try replacing "lightness" with "light" in a discussion about color and see how much sense the text makes afterwards.
If I understand the argument, he's saying that rather than use branches to gate what code actually is live in production, you should use feature flags or some other modularizing system, and have a discipline on your team where features that take weeks to develop should be introduced in a series of short-lived (~1 day) branches that don't break the build because their code is hidden by a feature flag (or something) until the whole feature is done.
More or less. I'm inclined to agree. More than once, I've been burned when merging a long-lived feature branch, because it turns out that, during development, the behavior of some other portion of the system changed in a way that broke the behavior of what I was working on, without actually causing any compile-time or run-time errors.
You can theoretically guard against that with more and more automated testing, but that's putting a lot of weight on a thing that's almost always full of things you forgot to test, or didn't think you needed to test.
I've had better luck with feature flags. They don't take anything away from automated tests or static analysis. They potentially add a little bit more hassle in the form of maintaining the flags. (Not nearly as much hassle as sorting out a merge conflict, though.) The other, quite compelling, thing they add is a much wider expanse of time during which you can notice, "Huh, that's funny," when you're doing your mid-development ad-hoc testing.
You can, of course, get a similar benefit by making a daily practice of rebasing your feature branch onto the latest version of the main branch. But my experience has been that doing that is annoying, and the thing about annoying things that you have to actively remember to do is that it's surprisingly easy to just not remember to do them.
Feature flags make functional overlap and branching explicit and prevent difficult merge conflicts. They're easier for humans to reason about, and far easier/safer to do percentage rollouts with.
I understand it that way as well. Unfortunately if everyone isn't sufficiently disciplined on a team, what happens is that code that takes weeks to develop sits on their machine for weeks. Which is like a feature branch that nobody can see.
That, plus a few sentences later: "Frankly, at the time, I did not understand much about branching strategies. It all seemed too complicated to me. I had hard times trying to fit the workflow in my head."
I have no idea whose blog I'm reading here, but he doesn't exactly sound like someone whose opinion on version control and branching strategies should count for very much.
Coming next week - a post about why X is the best programming language, from someone who hasn't bothered to learn any other programming languages!
Having worked with novice developers very recently I agree with the author here. The workflow of SVN (and similar) is much easier to understand and explain to folks who haven't used version control before. People tend to easily lose the thread when there's a local version of main/master, a remote version, and a local and remote version of their own branches, and they have to perform a whole host of commands to keep everything the way it should be. I often find even a simple workflow involves teaching multiple Git concepts that get in the way.
I don't get how that's so complicated to be honest and your host of commands is literally 3. SVN had 2, big deal, especially when of those 2, commit can destroy all your work easily n SVN, while a very simple rule of git will allow you to never loose any work: Before you do "anything else", commit!
By simple workflow I mean the simple workflow that mimmicks what you would've done with SVN (or CVS for that matter). Remember the holy wars of SVN over CVS? "What do you mean the entire repo has one version number instead of each file? Are you insane!" :)
What git allows for are different workflows and some of them are probably harder to understand than others but very workable ones are really easy too and don't require a host of commands or a deep understanding as long as simple rules are followed. All of my last few new hires (juniors) can work without problems with our simple git workflow. FWIW, we use short lived branches per ticket that get PR'd into master. Master is always green and deployable (hourly). The branch is yours to do with whatever you want until you want to actually merge to master, at which point you have to squash and rebase from master.
Some guys got bitten by not following the simple rules of "do one thing at a time" and "commit first". Luckily git refuses many actions if you have a dirty tree but people can get confused because they never read error messages. "Yes, yes I pulled and then ... and now it doesn't do what it's supposed to". Do one thing at a time works for most things in life and software development and rebasing from master while also squashing at the same time is just insane. Just do one of them first. Usually I find squashing first is easier as otherwise you might have to solve conflicts on intermediary commits that you wouldn't have in the first place with your squashed commit.
You don't have to convince me that the git workflow is more powerful and affords more opportunities to recover from error - I completely agree and could go on at length.
However, the git workflow requires you to understand a larger number of concepts and I can personally attest that these concepts are not intuitive to new learners. For example, in Git you need to do the following, just to create a PR editing a file:
1. Checkout master
2. Pull master
3. Create a new branch off of master
4. Edit files
5. Stage edits
6. Commit
7. Push the local branch to the remote
Consider the equivalent SVN workflow:
1. svn update
2. edit files
3. svn commit
Notice what I didn't have to understand there. There's no branches, no division between local and remote, no index and distinction between staged and unstaged changes.
All of these concepts are like an unexpected speed bump when someone is trying to learn to accomplish a task; they're trying to do one thing at a time and are forced to instead learn 5 other things until those are fully reinforced. And we haven't even gotten into the various minutia of commit-squashing etiquette, rebasing vs. merge commits, and all the other powerful abstractions that simply don't exist in SVN.
1. You're comparing a branching workflow in Git to a branchless workflow in SVN. The branchless workflow in Git eliminates step 1 and 3.
2. You can remove step 5 by doing `git commit -a` in step 6. Sure, you will have to `git add` new files, but you need to do the same in `svn add`, which your list for SVN conveniently ignores.
Now at this point in the simple workflow, yes you do _have_ to learn something new. You have to learn that git is always offline and you do need to actively push your commit.
git push
That's it. That is the _only_ difference to doing the same workflow you did with SVN with git.
Now if you were using SVN with a code review workflow, it is very likely that you used something like Atlassian's Crucible and you would use branches. Let's say you just for the first time pulled/did the initial svn up to get the code and now you're gonna work on your ticket using a code review based workflow with branches. So on svn you have trunk checked out and on git you are on master.
svn up <whatever> = git pull (this can be skipped in both, if you just svn upped/cloned for the first time before)
svn copy trunk branches/<branchForMyTicket> = git checkout -b <branchForMyTicket>
edit files, create new files etc.
svn add <files> = git add <files>
svn commit -m "commit msg" = git commit -a -m "commit msg"
At this point the same difference comes in that you already learned in the non code review workflow. You push, big deal, already learned.
git push
As the sibling pointed out, since we're using the command line here for the example, adding new files is the same for both, while editing existing files can be done differently. I.e. with svn you could skip the svn add, because svn commit will commit all changes files. With git you will have to learn to just always do `git commit -a`, which you might even alias. Personally I just add it myself when needed because sometimes I do _not_ want to do that. But I can totally understand why a newbie might not want to.
Now if we get away from the command line and just think in concepts that someone has to understand on top of svn while using a GUI, there really isn't much more than the "You need to push to actually publish your changes instead of commit always talking to the server right away". Most GUIs for version control present these simple workflows in _exactly_ the same manner, i.e. they do an implicit "git add" and "git commit -a" for you by default and you can select which files to actually commit by checking and unchecking checkboxes just like they allowed you to do for SVN.
Now we're actually at a problematic stage. How do you merge your branch into trunk/master? Maybe your code review tool allows for it via the web UI or maybe you need to do it yourself on the command line/GUI locally and push. This is where you will find that it's a pain in the rear with SVN, including lots of conflict potential, slow operations etc. and git will just be a breeze.
I've worked with many new developers, some self-taught on the side, some of them directly out of bootcamps and only been programming for a few months.
Sure, they don't all know the ins and outs of the git data model and what every command does. But I've never seen anyone that couldn't understand git pull, git commit, and git push. If the org uses a branching model, you can teach them git checkout -b, git checkout <branch_name>, and git merge origin/master to switch between branches in 5 minutes. They might get themselves into trouble eventually, but then someone more experienced can come by with git reflog and help them recover their work. Set up their github project to use squash commits only so each PR is one commit and master is always linear and they're off to the races.
Despite some command naming quirks, the simple stuff is pretty simple in git and the more advanced stuff is there when you want to learn it. I really don't buy that there is any good reason to choose SVN these days, regardless of how junior the team might be.
I genuinely don't get how you can rely primarily on unit tests to prevent regressions in your code. They are at such a fine granularity that most significant changes require restructuring the "units" in a way that invalidates the tests. Then we're left with these aenemic E2E tests (because fast!) that only tell you if something basic is broken.
These CD advocates seem to be saying that you can take human out of the testing equation and rely solely on automation. I just don't buy it.
I haven't seen true continuous delivery in the wild and I'm starting to think this is just the empty promises of a club of consultants who peddle their wares to unsuspecting VPs. Tell me what I'm missing.
I rewrote the backend on a team I used to work on. The service had a ton of unit tests. Given that this was a full rewrite, those unit test were useless. I spent the first few days writing a comprehensive suite of integration tests I could run against the existing service. These tests directly mimicked client calls, so the same tests should be just as valid for the rewritten service. Using these tests, I was able to catch 90%+ of potential issues before cutting over to the new service.
Personally, I find unit tests to be mostly useless. Every time I touch code with a unit test, I also need to change the unit test. Rather than testing, it feels like writing the same code twice.
Unit tests help verify individual components of a system - which makes them top-of-mind for library code.
I think the issue with them lies in that most developers aren't shipping libraries, they're shipping integrated systems, so there's no component worth testing. (you can always invent one, but that's just overcomplicating the code).
At the same time, it's also genuinely hard to write good, principled tests of integrated systems, harder than it is to code up a thing that kinda-works and then manually debugging it enough to ship. You have to have the system set up to be tested, and feature complexity actively resists this - you fight a losing battle against "YOLO code" that gets the effect at the expense of going around the test paradigm.
I've arrived at this exact same conclusion for frontend work as well. I always go for integration tests first, and only rely on unit tests if hitting some edge case is hard via integration test.
And to clarify, if any individual function reaches some arbitrary level of irreducible complexity, then I'll absolutely unit test that. It's kind of a "you know it when you see it" kind of thing.
How does this scale though? If you've got integration tests that include state, now you've got to either run your tests serially or set up and tear down multiple copies of the state to prevent tests from clobbering each other. As your project expands, the tests will take longer and longer to run. Worse, they'll start to become unreliable due to the number of operations being performed. So you'll end up with a test suite that takes potentially multiple hours to run, and may periodically fail just because. The feedback loop becomes so slow that it's not helpful during actual coding. At best, it's a semi-useful release gate. Is there another way?
I've seen this more often than not. The pyramid is a pyramid for a reason, and I'm somewhat skeptical that we just have to throw out unit tests or they suck or something.
You don't have to be a TDD acolyte to find unit tests useful or essential.
> If you've got integration tests that include state, now you've got to either run your tests serially or set up and tear down multiple copies of the state to prevent tests from clobbering each other.
That is a very normal setup.
> Worse, they'll start to become unreliable due to the number of operations being performed. So you'll end up with a test suite that takes potentially multiple hours to run, and may periodically fail just because.
This is called flakiness and is generally a symptom not to be ignored, as it is almost always indicative of bigger issues. It's rare that flakiness is limited to test environments. Instead it's much more likely that whatever your smoke tests are experiencing is a something end-users are also intermittently hitting.
> The feedback loop becomes so slow that it's not helpful during actual coding.
Devs can write their own unit tests when working on their assigned tasks. Smoke tests are designed to run when you're trying to integrate those changes into the existing codebase. At that point, you have the calculus all wrong. Smoke tests slow down devs enough that they don't merge broken code into production. That is a useful release gate unto itself.
If unit tests pass but smoke tests fail, then often (the vast majority of the time in my experience) the issue is that either the dev didn't understand the task or, more often, didn't understand the system they were integrating into.
> Personally, I find unit tests to be mostly useless.
Meanwhile I've found myself in the position where people eschewed unit testing entirely under this "just do integration tests" thinking and now I'm stuck fumbling around with flaky and hard to run integration tests that take forever to run.
Unit tests are good because they test whether the code you've written work as you expect. They run instantly. They don't require a full setup with other integrations.
This is indispensable - while it's nice to see a service integrating well and behaving as expected, it's also very useful to know practically instantaneously that the logic you're calling from a controller or something isn't broken. And if it's really tough you can easily put in breakpoints and step through, etc.
I'm a game dev and over time I've settled on using two groups of tests for my projects. Both at opposite ends of the spectrum.
.
1. Unit tests. But I only write them for stuff that needs them.. Eg. Some complex math functions that translate between coordinate systems; the point of the unit tests is to confirm that the functions are doing exactly what I think they are doing. With mathsy stuff it can be very easy to look at the output of some function and think, that looks fine, but in reality its actually slightly off, and not exactly what it should be. The unit tests are to confirm that its really doing what I think its doing.
.
2.Acceptance tests by a human. Theres a spreadsheet of everything you can do in the game and what should happen. Eg. press this button -> door should open.. As we add features we add more stuff to this list. At regular intervals and before any release several humans try every test on various hardware. This is to catch big / complex bugs and regressions. Its super tedious but it has to be done imo. Automating this would be an insane amount of work and also pointless as we are also testing the hardware, you get weird problems with certain GPUs, gamepads, weird smartphones etc.
.
I find those two types of tests to be essential, the bare minimum. But also anything in between, like some kind of automated integration testing is just a shittone of work and will only be useful for a relatively brief period of development, changes will quickly render those sort of tests useless.
> Personally, I find unit tests to be mostly useless. Every time I touch code with a unit test, I also need to change the unit test. Rather than testing, it feels like writing the same code twice.
I think they're mostly useless when refactoring, but they're useful when writing new code and and making relatively small to medium sized changes. For new code, it's helpful to me at least to express my intentions in a more concrete form and it gives me more confidence that I didn't miss something. For making relatively small changes, they help catch fined-gained regressions. Even if I meant to make a change, a failing test forces me to think about handling a particular case correctly that I might have forgotten.
The kind of unit test I do hate are the ones that are so mock-heavy that they're pretty much only testing the structure of your codebase (did you call all the methods the right order and nothing more?). I was once on a team where that was pretty much all they wrote, and they were very resistant to any level of integration unit testing because (I think) they read in a opinionated book somewhere that low level tests were good enough (they weren't).
Except you often need to rewrite them, so now you've got two places (per 'unit') where you could have introduced a bug. Integration tests and E2E tests are far more valuable because they're attacking it at the business logic side, which is far less volatile, and particularly in a refactor, a useful invariant.
> Except you often need to rewrite them, so now you've got two places (per 'unit') where you could have introduced a bug.
That's not a bad thing, though. DRY might be fine for your main implementation, but redundancy is a time-tested way of catching errors (a.k.a. "double checking").
In theory, if adequate attention is spent on both maintaining the implementation as well as the tests, this is perfectly valid. In practice, this trade-off between expedience and verification goes towards the former when it comes to rapid development.
I often feel that people take "unit" test too literal and E2E as well. You can write perfectly valid, fast and useful partly integrated tests with common unit testing frameworks.
The other thing is that like you and come siblings have pointed out, many if not most people write unit tests all wrong and in the end just test the mocks. Those are really bad and you can just throw them away. Same with all those tests that just check that the right internal calls are being made. Tests nothing.
You need to attack the "business end" of your unit (or small groups of units). Inputs in and assert the outputs. Asserting that a certain collaborator was called can still make sense but if that's literally the only thing you do it's not very valuable at all.
You can generally see whether a unit test was a good unit test based on the fact that you were able to refactor the implementation of the method _without_ having to change the unit test. Yes, those definitely do exist, even in larger systems.
> I often feel that people take "unit" test too literal
Perhaps. And this is where I think a useful distinction can be made between the "unit" (usually a function, or sometimes a single file, e.g., a C-style compilation unit) and a "system" (a collection of functions that perform complementing tasks, sometimes also a unit).
> You need to attack the "business end" of your unit (or small groups of units)
It's worth noting here, that sometimes the business logic extends all the way to the "unit". This is usually in very technical domains. Like say, writing a maths helper library for consumption by other programmers (either internally or externally) would often have a clearly specified outcomes at the unit level.
> You can generally see whether a unit test was a good unit test based on the fact that you were able to refactor the implementation of the method _without_ having to change the unit test. Yes, those definitely do exist, even in larger systems.
We might be in different types of software development. There's seldom an actual "specification" to a level of detail that you could test to in the sense you're probably thinking of (but I'm having to guess here for lack of detail and context from your end).
In the field I work in for example, a detailed specification in the way I'm guessing you mean would be prohibitively expensive and just not cost effective at all vs. the benefit you can get from throwing something together from imperfect information and improving upon it iteratively.
There was a (WP?) article on HN recently about "Releaseing software the right way" (or similar title), which basically said to use whatever approach actually makes sense in your circumstances. The example IIRC was hardware development (detailed specs) vs. a SaaS company.
In terms of something like embedded systems, aerotech, etc., the specification extends all the way to the unit. In terms of an SaaS, the specifications extend all the way to business logic such as (cartoon examples):
- don't charge the customer twice
- or when I click submit on the front-end the following possibilities happen according to the back-end response
- or when the back-end receives x, the inventory should be updated according to this business logic, as well as y
Say if you're doing this capital-A Agile style, all of these should be present on the acceptance criteria for any user story. As someone who's worked in rapid applications development for mobile, I can say reaching this level of specification increases speed, and reduces redundant communication. It often doesn't take half an hour for someone to write, and then it's iterated on by the team before implementation, and during.
If you have some code that if its callers changed, they would stop using that code or use it on a different place, it's a unit and it's a good idea to unit test it.
If you have some code that if its callers changed you would want to change it too, then it's on the same unit as the calling code, and it's bad to divide it away.
We deploy to production multiple times per day, with no human intervention, successfully.
Our test suite includes some unit and full end to end tests, and yea sometimes a breaking change slips through, we write a test to catch it next time, and move on.
(Though our deploy process involves spinning up new instances to run our code, and our load balance won’t switch over to new boxes if they return 500s, so fully broken deploys don’t actually get hit by customers.)
Different projects have different complexities and differing levels of CD viability, but there are definitely many people truly doing automated continuous deployment all day every day.
what counts as "C" in CD? FANG push changes to each of many components daily, with feature flags. of course each change goes through stages: dev, preprod, prod over a few days.
> I genuinely don't get how you can rely primarily on unit tests to prevent regressions in your code. They are at such a fine granularity that most significant changes require restructuring the "units" in a way that invalidates the tests.
Besides less-anemic E2E tests, this is why type systems exist.
Define a well-typed interface, code to the interface, test the units — and then it's hard to make terrible mistakes wiring things up. To realize the full benefit does, of course, require substantially better data structures than string-to-Any hashes, and it requires some proper old-school software engineering (i.e. injecting a client as an argument and not just mocking out the someOtherClient.client() method.) And there is some overhead, though I'd say that a proper null-checking pass alone would be worth that much. (Sorry, Java, your types are terrible.)
I’ve done CD in production. If you have “enough” test automation, you can have enough confidence that significant regressions become rare, and when they do crop up you can find and fix them in minutes-to-hours from finding the bug to releasing a fix to production. It works very well if you can tolerate that some of your users will have (typically minor) issues once in a while. Coupled with blue/green or canary deployments you can move pretty quickly while minimizing the impact of bugs.
I don't agree that you can take humans out of the equation, particularly for new code.
but functional programming makes unit tests the natural way to regression-test the top 80 to 90 percent of your codebase, and those don't have to target tiny pieces - they can target large units.
I advocate for lots of short-lived branches, and use something like Git's Pull-Request process to do peer-review. He knocks GitFlow which is something else entirely IMO where you have long-lived branches that never get deleted. If you're doing trunk-based development how does code ever get reviewed first? or it just doesn't?
The pull request flow is fantastic, especially if you have a tool like GitHub’s “Ready to Merge” that shows you if the code can be merged in without conflict.
This puts the burden of branch maintenance/compatibility on the branch owner, and allows a clean and clear place for code review.
Pull Requests are great, but i think they can be a serious problem if you have to merge between multiple release branches, with testing and approval for each release branch.
Using Trunk-based development with feature flags seems to be me to be the best way of ensuring success with a Pull Request model.
Trunk based can be sort of ok of you can accept a broken trunk and features can be kept small enough to give meaningful commits. If features require 10 commits and you have several teams working in parallel on different features then you’ll have an interleaved history of various stages of completion of each feature. You need to make each step of each feature shippable if you use CD - so e.g you can’t commit the removal of an old implementation and later add the new implementation.
In all, I’d say the pain of feature branches and PRs is tiny compared to trunk based development, even for teams as small as two.
I make a point never to commit broken code anyway. If a series of changes have to be together to not break, I put them all in one commit. And for organization’s sake, commits could still be tagged by ticket
That doesn't mean an entire feature has to go in one commit (usually). Often a feature is made up of a series of non-breaking changes that can be layered one at a time, if appropriate.
My team makes several commits locally, then rebases, squashes, and pushes the squashed commit to the trunk which has CI. This maintains a local, regular backup of your code and collects features into single commits for the trunk which are easily revertable if found to be problematic. This flow will not work for everyone, but it works very well for us (a team of 8 at a FAANG company).
Edit: Rereading your comment, I suspect you mean non-local backups. Our organization has a special remote only visible to you and those you allow for pushing code you'd like to backup off of your box.
That sounds like feature branches but with more steps and worse. You can't share your unfinished work non-locally, for one thing. And it has none of the advantages of CI because you still have infrequent large merges.
How about with multiple repos, across multiple systems? Eg. embedded device with 4-week test cycles, backend server with CI/CD, frontend site with CI/CD, and upstream vendor contribution PR(s), and where the data fees for firmware updates to the set of all customer embedded devices run into the hundreds of thousands of dollars? You can't get them all in one commit then. Even with a monorepo (not contributing back to upstream) it'd be an utterly massive commit.
Going back through history to find where a bug was introduced usually happens on the commit level (e.g. git-bisect only operates on commits, naturally). Once you find the commit that introduced the bug in question, it will be immensely easier to hunt down the bug within the commit if the commit is 10 or 50 lines long instead of 500 or 1000.
As a reviewer I really want a series of commits each making closely related changes that are easy to understand. Squashing tends to make giant diffs that replace almost every line, and you almost have to reverse-engineer to figure out what’s actually supposed to be different.
Feature flags can be helpful here. Even if the feature is incomplete, it’s still ok to commit it.
Another thing that can help is a source control system that supports multiple commits and committing them to the main branch as a contiguous unit. I believe Phabricator supports that.
The next best thing you can do with GitHub is make your pretty series of commits for review, but then squash on completion using the GitHub UI. GitHub retains the individual commits of the PR that we’re reviewed while the main branch history has a single commit that passed CI.
As much as I love tdp, he has been promoting the same topic for years. Time to change and start talking about... euh... CICD promotions considered evil!
For us, two places. When the feature is ready to be merged in, it is reviewed by the scrum team. When they say a set of code is ready for production, it gets tagged - and that tag gets the formal peer review.
> When arriving, I discovered branches were living from 5 days to somewhere 30 days. People had to rebase mainline frequently into their branches, causing lots of rework and spending much non-valuable time on fixing what got broken.
This alone doesn't tell me anything about their process. It happens with unbranched workflows as well (unless you use a VCS that prevents more than one person from checking out a file at one time). The fact is, some work takes a few days (or weeks) to get right, and in the meantime other people may have changed those files. This is why `svn update` and `p4 sync` exist.
Version control and specifically branching strategy (which affects how code is reviewed) has a huge dependency on team size and modularity. Once you go past a certain number of people working on the same module, it’s improbable to have longer living feature branches. You need to have feature flags, if only to make it possible to stay sane (and not necessarily for toggling features exposed to users).
So the trick is to figure out where (which side) you are and will be and utilise the right strategy.
The OP is incorrect, most of the large organisations he cites do not practice trunk-based development.
Google has a monorepo and what could describe as 'submit patch for code review'. Only certain engineers can approve reviews and trigger the patch being applied to the trunk.
Many of the others also have variations, but unlikely that any organisation with 1000+ engineers has a monorepo with all of those engineers directly committing to it.
They never mention the little details in articles like this, like the fact that TBD relies entirely on CI. So what does that mean?
- CI requires an automation tool to run a test suite. If you don't have one yet, you need to set one up (and, you know, have a suite of tests to run). hand-waves the idea that the test suite is perfect
- If you don't have Pull Requests to gate the test suite before merge to trunk, the tests have to be done pre-merge. But that means it has to run on your laptop, and maybe there's too many tests. So you can do 'fake' Pull Requests, by keeping a CI server with one copy of your repo, and if the tests pass, pushing that copy or change to the 'real' repo.
- Even if you do have Pull Requests, or Fake Pull Requests, and a person has to approve them before merge (a good idea), you can still end up with a PR stuck in pending-approval state for eternity. And every time a change hits the trunk, the PR needs to be rebased, retested, and possibly bugs fixed. This means the equivalent of a branch being open for a long time - the whole reason not to use feature branches.
- Therefore, assuming you have a test suite, and a CI server, you must only merge very small changes, quickly. Not only must the change be small, you must have review & approval constantly. Or throw out the review/approval step. Or only do it once a day, which goes back towards the long-lived branches that we're trying to avoid.
- Of course with all this fast merging, it's increasingly likely you will introduce a bug, because no testing is perfect. So you have to also back out changes quickly. But you still won't always catch them quickly.
- With all these changes and reverts happening all the time, the trunk is constantly changing underneath everybody. So you have to rebase or merge constantly. If you don't, it will take you longer to refactor the changes you've just developed to work with the new changes.
- And since you can't possibly implement a complex new feature at once with only small commits, you have to use feature flags and other strategies to keep your untested new code from exploding until you think it's ready for prime time.
- As all of that is going on, you also of course need to test all this crap. So you'll need a CD system designed to constantly deploy to various environments, potentially managing many constantly changing dependencies and data sets depending on what's being pushed/tested.
- That system has at least the same complexity as the CI system, but it's infrastructure-heavy, which means you'll need to lean heavily on DevOps practices and SWE having the tools and training to manage their own deploys/infra. And we haven't even talked about versioning.
These are just some of the issues that some books cover (I think) but quaint blog posts about branching never mention. How long will it take your team(s) to implement all of that and completely change their SDLC?
But when the version control system is distributed you already have a form of "feature branches" since everyone has a local copy with the changes they're working on. The question is perhaps more about if it's okay to push to trunk or not (without having a review process)?
Apologies for comment thread hijacking, but I have a related question I hope someone could help me understand.
I have worked on many teams that did continuous integration, but never continuous deployment. With continuous deployment, say I branch off trunk and start to implement FeatureA. Just after that, someone else branches off trunk, implements a smaller FeatureB, and merges it back in to trunk. The next day I finish FeatureA and open a pull request back to trunk.
All unit tests pass on the code I submitted, and there are no conflicts with trunk. But still, it's not going to be a fast forward merge, so the code I submitted is different than what will be actually be deployed after FeatureA is merged to trunk. And maybe after merge, certain unit tests will fail on trunk. This same truth applies regardless if my pull request is set to squash-merge or rebase.
So my question. When do unit tests automatically get run?
1. On my feature branch after the pull request is opened?
2. On trunk after my feature branch is merged?
3. Both 1 and 2?
4. Or am I, as a submitter of code required to continually update my pull requests such that they are always going to be fast forward? That is, if I am waiting on a pull request to be merged, and someone else's PR is merged do I have to rebase my code on top of trunk every time?
|
|\
|\\
|||FeatureB
|//
|/
* Passing unit tests in FeatureB do not guarantee passing unit tests here
89 comments
[ 3.6 ms ] story [ 171 ms ] threadCan't even complain that it's a neologism and kids these days with their bad grammar; "yfelnyss" would appear to predate the Normans.
Nor is "evil" in the noun sense a great synonym for "evilness" in the noun sense. "Evilness" denotes the state of being bad, while "evil" denotes the badness itself. Evilness is the correct word here. Using "evil" would have been, if we want to be really pedantic (I hope I can safely assume that we do), been mildly nonsensical.
[EDIT:] However, I can't agree that TFA is about "the state" of feature branching, or really even that feature branching is an entity that can possess a state. According to TFA, feature branching is bad. That's an intrinsic property, not one that feature branching will be able to overcome with appropriate prayer and reflection.
This is usually the case when abstract nouns are considered evil, so "evilness" is usually poor diction.
Indeed, we could not say "the black of feature branches"; it would have to be "blackness". Something that is black (adjective-wise) doesn't "have black" (the noun); it "has blackness".
The word "evil" doesn't conform to this restriction; "the evil of feature branches" is fine. Therefore "evilness" is just superfluousnessitude.
The word "bad" is color-like. We need "the badness of feature branches", not "the bad of feature branches".
But "evil" functions in such a way that it can substitute for "badness", and therefore for "evilness".
When we say "the evil of feature branches", the grammar is very similar to "the hypocrisy of using feature branches" or "the iniquity of using feature branches" or "the stink of feature branches" and such.
None of these words require an additional suffix; "stink" need not be worked into "stinkiness".
For an analogous example, try replacing "lightness" with "light" in a discussion about color and see how much sense the text makes afterwards.
Yeah, that'll get really weird really fast, what with "lightness" being about weight and not colour, and all...
You can theoretically guard against that with more and more automated testing, but that's putting a lot of weight on a thing that's almost always full of things you forgot to test, or didn't think you needed to test.
I've had better luck with feature flags. They don't take anything away from automated tests or static analysis. They potentially add a little bit more hassle in the form of maintaining the flags. (Not nearly as much hassle as sorting out a merge conflict, though.) The other, quite compelling, thing they add is a much wider expanse of time during which you can notice, "Huh, that's funny," when you're doing your mid-development ad-hoc testing.
You can, of course, get a similar benefit by making a daily practice of rebasing your feature branch onto the latest version of the main branch. But my experience has been that doing that is annoying, and the thing about annoying things that you have to actively remember to do is that it's surprisingly easy to just not remember to do them.
Feature flags make functional overlap and branching explicit and prevent difficult merge conflicts. They're easier for humans to reason about, and far easier/safer to do percentage rollouts with.
This author has much to teach us about evil after all
I have no idea whose blog I'm reading here, but he doesn't exactly sound like someone whose opinion on version control and branching strategies should count for very much.
Coming next week - a post about why X is the best programming language, from someone who hasn't bothered to learn any other programming languages!
By simple workflow I mean the simple workflow that mimmicks what you would've done with SVN (or CVS for that matter). Remember the holy wars of SVN over CVS? "What do you mean the entire repo has one version number instead of each file? Are you insane!" :)
What git allows for are different workflows and some of them are probably harder to understand than others but very workable ones are really easy too and don't require a host of commands or a deep understanding as long as simple rules are followed. All of my last few new hires (juniors) can work without problems with our simple git workflow. FWIW, we use short lived branches per ticket that get PR'd into master. Master is always green and deployable (hourly). The branch is yours to do with whatever you want until you want to actually merge to master, at which point you have to squash and rebase from master.
Some guys got bitten by not following the simple rules of "do one thing at a time" and "commit first". Luckily git refuses many actions if you have a dirty tree but people can get confused because they never read error messages. "Yes, yes I pulled and then ... and now it doesn't do what it's supposed to". Do one thing at a time works for most things in life and software development and rebasing from master while also squashing at the same time is just insane. Just do one of them first. Usually I find squashing first is easier as otherwise you might have to solve conflicts on intermediary commits that you wouldn't have in the first place with your squashed commit.
However, the git workflow requires you to understand a larger number of concepts and I can personally attest that these concepts are not intuitive to new learners. For example, in Git you need to do the following, just to create a PR editing a file:
1. Checkout master
2. Pull master
3. Create a new branch off of master
4. Edit files
5. Stage edits
6. Commit
7. Push the local branch to the remote
Consider the equivalent SVN workflow:
1. svn update
2. edit files
3. svn commit
Notice what I didn't have to understand there. There's no branches, no division between local and remote, no index and distinction between staged and unstaged changes.
All of these concepts are like an unexpected speed bump when someone is trying to learn to accomplish a task; they're trying to do one thing at a time and are forced to instead learn 5 other things until those are fully reinforced. And we haven't even gotten into the various minutia of commit-squashing etiquette, rebasing vs. merge commits, and all the other powerful abstractions that simply don't exist in SVN.
1. You're comparing a branching workflow in Git to a branchless workflow in SVN. The branchless workflow in Git eliminates step 1 and 3.
2. You can remove step 5 by doing `git commit -a` in step 6. Sure, you will have to `git add` new files, but you need to do the same in `svn add`, which your list for SVN conveniently ignores.
You are fogetting something in your three step svn tutorial: svn add!
Now at this point in the simple workflow, yes you do _have_ to learn something new. You have to learn that git is always offline and you do need to actively push your commit. That's it. That is the _only_ difference to doing the same workflow you did with SVN with git.Now if you were using SVN with a code review workflow, it is very likely that you used something like Atlassian's Crucible and you would use branches. Let's say you just for the first time pulled/did the initial svn up to get the code and now you're gonna work on your ticket using a code review based workflow with branches. So on svn you have trunk checked out and on git you are on master.
At this point the same difference comes in that you already learned in the non code review workflow. You push, big deal, already learned. As the sibling pointed out, since we're using the command line here for the example, adding new files is the same for both, while editing existing files can be done differently. I.e. with svn you could skip the svn add, because svn commit will commit all changes files. With git you will have to learn to just always do `git commit -a`, which you might even alias. Personally I just add it myself when needed because sometimes I do _not_ want to do that. But I can totally understand why a newbie might not want to.Now if we get away from the command line and just think in concepts that someone has to understand on top of svn while using a GUI, there really isn't much more than the "You need to push to actually publish your changes instead of commit always talking to the server right away". Most GUIs for version control present these simple workflows in _exactly_ the same manner, i.e. they do an implicit "git add" and "git commit -a" for you by default and you can select which files to actually commit by checking and unchecking checkboxes just like they allowed you to do for SVN.
Now we're actually at a problematic stage. How do you merge your branch into trunk/master? Maybe your code review tool allows for it via the web UI or maybe you need to do it yourself on the command line/GUI locally and push. This is where you will find that it's a pain in the rear with SVN, including lots of conflict potential, slow operations etc. and git will just be a breeze.
Sure, they don't all know the ins and outs of the git data model and what every command does. But I've never seen anyone that couldn't understand git pull, git commit, and git push. If the org uses a branching model, you can teach them git checkout -b, git checkout <branch_name>, and git merge origin/master to switch between branches in 5 minutes. They might get themselves into trouble eventually, but then someone more experienced can come by with git reflog and help them recover their work. Set up their github project to use squash commits only so each PR is one commit and master is always linear and they're off to the races.
Despite some command naming quirks, the simple stuff is pretty simple in git and the more advanced stuff is there when you want to learn it. I really don't buy that there is any good reason to choose SVN these days, regardless of how junior the team might be.
Hey, that was me-- wasn't that all of us, after we learned our first programming language?
These CD advocates seem to be saying that you can take human out of the testing equation and rely solely on automation. I just don't buy it.
I haven't seen true continuous delivery in the wild and I'm starting to think this is just the empty promises of a club of consultants who peddle their wares to unsuspecting VPs. Tell me what I'm missing.
Personally, I find unit tests to be mostly useless. Every time I touch code with a unit test, I also need to change the unit test. Rather than testing, it feels like writing the same code twice.
I think the issue with them lies in that most developers aren't shipping libraries, they're shipping integrated systems, so there's no component worth testing. (you can always invent one, but that's just overcomplicating the code).
At the same time, it's also genuinely hard to write good, principled tests of integrated systems, harder than it is to code up a thing that kinda-works and then manually debugging it enough to ship. You have to have the system set up to be tested, and feature complexity actively resists this - you fight a losing battle against "YOLO code" that gets the effect at the expense of going around the test paradigm.
You don't have to be a TDD acolyte to find unit tests useful or essential.
That is a very normal setup.
> Worse, they'll start to become unreliable due to the number of operations being performed. So you'll end up with a test suite that takes potentially multiple hours to run, and may periodically fail just because.
This is called flakiness and is generally a symptom not to be ignored, as it is almost always indicative of bigger issues. It's rare that flakiness is limited to test environments. Instead it's much more likely that whatever your smoke tests are experiencing is a something end-users are also intermittently hitting.
> The feedback loop becomes so slow that it's not helpful during actual coding.
Devs can write their own unit tests when working on their assigned tasks. Smoke tests are designed to run when you're trying to integrate those changes into the existing codebase. At that point, you have the calculus all wrong. Smoke tests slow down devs enough that they don't merge broken code into production. That is a useful release gate unto itself.
If unit tests pass but smoke tests fail, then often (the vast majority of the time in my experience) the issue is that either the dev didn't understand the task or, more often, didn't understand the system they were integrating into.
Meanwhile I've found myself in the position where people eschewed unit testing entirely under this "just do integration tests" thinking and now I'm stuck fumbling around with flaky and hard to run integration tests that take forever to run.
Unit tests are good because they test whether the code you've written work as you expect. They run instantly. They don't require a full setup with other integrations.
This is indispensable - while it's nice to see a service integrating well and behaving as expected, it's also very useful to know practically instantaneously that the logic you're calling from a controller or something isn't broken. And if it's really tough you can easily put in breakpoints and step through, etc.
.
1. Unit tests. But I only write them for stuff that needs them.. Eg. Some complex math functions that translate between coordinate systems; the point of the unit tests is to confirm that the functions are doing exactly what I think they are doing. With mathsy stuff it can be very easy to look at the output of some function and think, that looks fine, but in reality its actually slightly off, and not exactly what it should be. The unit tests are to confirm that its really doing what I think its doing.
.
2.Acceptance tests by a human. Theres a spreadsheet of everything you can do in the game and what should happen. Eg. press this button -> door should open.. As we add features we add more stuff to this list. At regular intervals and before any release several humans try every test on various hardware. This is to catch big / complex bugs and regressions. Its super tedious but it has to be done imo. Automating this would be an insane amount of work and also pointless as we are also testing the hardware, you get weird problems with certain GPUs, gamepads, weird smartphones etc.
.
I find those two types of tests to be essential, the bare minimum. But also anything in between, like some kind of automated integration testing is just a shittone of work and will only be useful for a relatively brief period of development, changes will quickly render those sort of tests useless.
I think they're mostly useless when refactoring, but they're useful when writing new code and and making relatively small to medium sized changes. For new code, it's helpful to me at least to express my intentions in a more concrete form and it gives me more confidence that I didn't miss something. For making relatively small changes, they help catch fined-gained regressions. Even if I meant to make a change, a failing test forces me to think about handling a particular case correctly that I might have forgotten.
The kind of unit test I do hate are the ones that are so mock-heavy that they're pretty much only testing the structure of your codebase (did you call all the methods the right order and nothing more?). I was once on a team where that was pretty much all they wrote, and they were very resistant to any level of integration unit testing because (I think) they read in a opinionated book somewhere that low level tests were good enough (they weren't).
That's not a bad thing, though. DRY might be fine for your main implementation, but redundancy is a time-tested way of catching errors (a.k.a. "double checking").
The other thing is that like you and come siblings have pointed out, many if not most people write unit tests all wrong and in the end just test the mocks. Those are really bad and you can just throw them away. Same with all those tests that just check that the right internal calls are being made. Tests nothing.
You need to attack the "business end" of your unit (or small groups of units). Inputs in and assert the outputs. Asserting that a certain collaborator was called can still make sense but if that's literally the only thing you do it's not very valuable at all.
You can generally see whether a unit test was a good unit test based on the fact that you were able to refactor the implementation of the method _without_ having to change the unit test. Yes, those definitely do exist, even in larger systems.
Perhaps. And this is where I think a useful distinction can be made between the "unit" (usually a function, or sometimes a single file, e.g., a C-style compilation unit) and a "system" (a collection of functions that perform complementing tasks, sometimes also a unit).
> You need to attack the "business end" of your unit (or small groups of units)
It's worth noting here, that sometimes the business logic extends all the way to the "unit". This is usually in very technical domains. Like say, writing a maths helper library for consumption by other programmers (either internally or externally) would often have a clearly specified outcomes at the unit level.
"Test the interface, not the implementation."
We might be in different types of software development. There's seldom an actual "specification" to a level of detail that you could test to in the sense you're probably thinking of (but I'm having to guess here for lack of detail and context from your end).
In the field I work in for example, a detailed specification in the way I'm guessing you mean would be prohibitively expensive and just not cost effective at all vs. the benefit you can get from throwing something together from imperfect information and improving upon it iteratively.
There was a (WP?) article on HN recently about "Releaseing software the right way" (or similar title), which basically said to use whatever approach actually makes sense in your circumstances. The example IIRC was hardware development (detailed specs) vs. a SaaS company.
- don't charge the customer twice
- or when I click submit on the front-end the following possibilities happen according to the back-end response
- or when the back-end receives x, the inventory should be updated according to this business logic, as well as y
Say if you're doing this capital-A Agile style, all of these should be present on the acceptance criteria for any user story. As someone who's worked in rapid applications development for mobile, I can say reaching this level of specification increases speed, and reduces redundant communication. It often doesn't take half an hour for someone to write, and then it's iterated on by the team before implementation, and during.
If you have some code that if its callers changed you would want to change it too, then it's on the same unit as the calling code, and it's bad to divide it away.
My last two jobs have had what I understand to be continuous delivery, to great success, but perhaps it was something other than CD.
Our test suite includes some unit and full end to end tests, and yea sometimes a breaking change slips through, we write a test to catch it next time, and move on.
(Though our deploy process involves spinning up new instances to run our code, and our load balance won’t switch over to new boxes if they return 500s, so fully broken deploys don’t actually get hit by customers.)
Different projects have different complexities and differing levels of CD viability, but there are definitely many people truly doing automated continuous deployment all day every day.
Besides less-anemic E2E tests, this is why type systems exist.
Define a well-typed interface, code to the interface, test the units — and then it's hard to make terrible mistakes wiring things up. To realize the full benefit does, of course, require substantially better data structures than string-to-Any hashes, and it requires some proper old-school software engineering (i.e. injecting a client as an argument and not just mocking out the someOtherClient.client() method.) And there is some overhead, though I'd say that a proper null-checking pass alone would be worth that much. (Sorry, Java, your types are terrible.)
but functional programming makes unit tests the natural way to regression-test the top 80 to 90 percent of your codebase, and those don't have to target tiny pieces - they can target large units.
This puts the burden of branch maintenance/compatibility on the branch owner, and allows a clean and clear place for code review.
Using Trunk-based development with feature flags seems to be me to be the best way of ensuring success with a Pull Request model.
In all, I’d say the pain of feature branches and PRs is tiny compared to trunk based development, even for teams as small as two.
It’s not the industry standard without reason.
That doesn't mean an entire feature has to go in one commit (usually). Often a feature is made up of a series of non-breaking changes that can be layered one at a time, if appropriate.
Edit: Rereading your comment, I suspect you mean non-local backups. Our organization has a special remote only visible to you and those you allow for pushing code you'd like to backup off of your box.
I'm sorry but it hurts just to read this. Just No.
Another thing that can help is a source control system that supports multiple commits and committing them to the main branch as a contiguous unit. I believe Phabricator supports that.
The next best thing you can do with GitHub is make your pretty series of commits for review, but then squash on completion using the GitHub UI. GitHub retains the individual commits of the PR that we’re reviewed while the main branch history has a single commit that passed CI.
This alone doesn't tell me anything about their process. It happens with unbranched workflows as well (unless you use a VCS that prevents more than one person from checking out a file at one time). The fact is, some work takes a few days (or weeks) to get right, and in the meantime other people may have changed those files. This is why `svn update` and `p4 sync` exist.
So the trick is to figure out where (which side) you are and will be and utilise the right strategy.
Google has a monorepo and what could describe as 'submit patch for code review'. Only certain engineers can approve reviews and trigger the patch being applied to the trunk.
https://github.com/google/eng-practices
Many of the others also have variations, but unlikely that any organisation with 1000+ engineers has a monorepo with all of those engineers directly committing to it.
- CI requires an automation tool to run a test suite. If you don't have one yet, you need to set one up (and, you know, have a suite of tests to run). hand-waves the idea that the test suite is perfect
- If you don't have Pull Requests to gate the test suite before merge to trunk, the tests have to be done pre-merge. But that means it has to run on your laptop, and maybe there's too many tests. So you can do 'fake' Pull Requests, by keeping a CI server with one copy of your repo, and if the tests pass, pushing that copy or change to the 'real' repo.
- Even if you do have Pull Requests, or Fake Pull Requests, and a person has to approve them before merge (a good idea), you can still end up with a PR stuck in pending-approval state for eternity. And every time a change hits the trunk, the PR needs to be rebased, retested, and possibly bugs fixed. This means the equivalent of a branch being open for a long time - the whole reason not to use feature branches.
- Therefore, assuming you have a test suite, and a CI server, you must only merge very small changes, quickly. Not only must the change be small, you must have review & approval constantly. Or throw out the review/approval step. Or only do it once a day, which goes back towards the long-lived branches that we're trying to avoid.
- Of course with all this fast merging, it's increasingly likely you will introduce a bug, because no testing is perfect. So you have to also back out changes quickly. But you still won't always catch them quickly.
- With all these changes and reverts happening all the time, the trunk is constantly changing underneath everybody. So you have to rebase or merge constantly. If you don't, it will take you longer to refactor the changes you've just developed to work with the new changes.
- And since you can't possibly implement a complex new feature at once with only small commits, you have to use feature flags and other strategies to keep your untested new code from exploding until you think it's ready for prime time.
- As all of that is going on, you also of course need to test all this crap. So you'll need a CD system designed to constantly deploy to various environments, potentially managing many constantly changing dependencies and data sets depending on what's being pushed/tested.
- That system has at least the same complexity as the CI system, but it's infrastructure-heavy, which means you'll need to lean heavily on DevOps practices and SWE having the tools and training to manage their own deploys/infra. And we haven't even talked about versioning.
These are just some of the issues that some books cover (I think) but quaint blog posts about branching never mention. How long will it take your team(s) to implement all of that and completely change their SDLC?
I have worked on many teams that did continuous integration, but never continuous deployment. With continuous deployment, say I branch off trunk and start to implement FeatureA. Just after that, someone else branches off trunk, implements a smaller FeatureB, and merges it back in to trunk. The next day I finish FeatureA and open a pull request back to trunk.
All unit tests pass on the code I submitted, and there are no conflicts with trunk. But still, it's not going to be a fast forward merge, so the code I submitted is different than what will be actually be deployed after FeatureA is merged to trunk. And maybe after merge, certain unit tests will fail on trunk. This same truth applies regardless if my pull request is set to squash-merge or rebase.
So my question. When do unit tests automatically get run?