charlesreid1.com blog

Git Workflows, Part 2: Crafting Commits

Posted in Git

permalink

Summary

  • Make your commits small and atomic, and recombine them into larger commits later; it's easier to combine smaller commits than to split large commits.

  • Make use of git add -p and git add -e to stage changes selectively and atomically.

  • Make use of git rebase and git cherry-pick to edit your commits and assemble them in the order you want.

  • Once commits have been combine and the history is satisfactory, push to a remote to share the work.

  • Think about ordering your commits to "tell a story". (What that means will depend on the people you are collaborating with!)

What is a commit

Before we get into the good stuff, let's talk about the anatomy of a git commit.

When you add files to your git repository, it's a two-step process: git add and git commit. The first step stages your changes, the second step memorializes those staged changes into a commit that can now be shared with others by pushing it to git remotes.

git add

It is important to know that git does not keep track of changes at the file level, it keeps track of changes at the character/line level.

What that means is, when you modify a line in a file that is in your git repository, and run git add to stage your change, git has created an object under the hood called a blob to represent that one line change.

If you change two lines in two different parts of a file, and stage those changes using git add, git will treat this as two separate changes, and represent the changes with two different blobs.

git commit

As you use git add to prepare your changes, the changes are added to a staging area. Think of this staging area as a draft commit. Each change being added to the staging area changes how the commit will look. When the changes are complete and the user runs git commit, it turns the staging area into a real commit, creates the metadata, and calculates hashes.

When a commit is created, it receives a name, which is the hash of the contents of the commit. The hash is computed from the contents of the blobs, plus the metadata about the commit, plus the hash of the prior commits. Changing a commit changes its hash, and will change the hashes of all subsequent commits.

Commits in your local repository can be easily rewritten and edited, and their hashes changed. A common workflow is to make many small commits, and recombine them later.

Because the commit hash is how the commit is named, modifying commits after you've shared them is bad practice and will create extra work for your collaborators. For that reason, don't git push until you're ready to share your work.

git rebase

The git rebase command allows you to edit your commit history. We will cover some usage patterns in the sections below.

git fetch and git pull

Before pushing changes to the remote, first check if there have been any commits since you began your branch.

rebase, merge, branch, pass

If a feature branch is created off of the master branch, and some time passes, the feature branch base commit may grow far out of sync with the master branch. (Note that master indicates the primary branch.)

This leaves the developer of the feature branch (which is out of sync with master) with a few choices:

  • rebase - continue to rebase all commits on the feature branch from the (old) original feature branch base commit onto the (new) head commit of the master branch.

    • Pros: clean history, easy for one-branch-one-developer workflow
    • Cons: requires continual force-pushes, requires coordination between developers to prevent squashing others' work, not scalable, some people hate this method
  • merge - occasionally merge work from the master branch into the feature branch.

    • Pros: simple to understand, simple to carry out, low cognitive load
    • Cons: any changes added to the branch via the merge commit will show up in the PR as new code, cluttering PR reviews by mixing features with merged changes; can also make the commit history messy and harder to understand.
  • branch - by making heavy use of throwaway branches and integration branches, it is easier to test out how the integration of a feature branch based on an old commit on master will do when merging it in with a newer version of master. Use throwaway integration branches to test out merging the two branches together, testing its functionality, etc. You can also rebase or cherry pick commits onto the throwaway integration branch, and figure out how to arrange the commits on a branch to "rebuild" it into a working, mergeable branch.

    • Pros: easy to do, encourages local use of throwaway branches
    • Cons: clutters branches, integration process has to be repeated (can be mitigated with git rerere), merge commits must wait until PR is approved
  • pass - best combined with the branch approach mentioned above, the pass approach is to leave the branch history clean, avoid force-pushes, and rely on throwaway branches to test out merge strategies once the inevitable PR merge needs to happen. It can also be useful to wait for code reviews to finish, then create a merge commit to make the merge happen smoothly.

    • Pros: easy to do
    • Cons: merge commits must wait until PR is approved

git push

Once you run git push, all of the commits on the branch that you pushed will end up on the remote, where others can access them. The purpose of a git push is to share commits, so generally you don't push branches until they are ready to share. This also allows more flexibility in crafting, rewriting, and combining commits.

force pushing

If you pushed a branch (which is a collection of commits) to a remote, and then you have edited those commits, you will run into a problem when you try and git push the new, edited versions of the commits to the same remote. The remote will detect that there are conflicting versions of the branch and will reject the changes.

That's where git push --force comes in. The --force flag tells the remote to discard its version of the branch and use the version of the branch that you are pushing.

We will cover more about force pushing - when to do it, when not to, and why some people hate it - in a later post. For now, we will only say that you should not force push often, since you can risk deleting others' work and creating additional confusion and work for all of your collaborators.

Commit Workflow

Principles

Here are some principles for your git commit workflow:

  • Commit small changes often.

  • Don't sweat the commit messages - they can be fixed up later.

  • Related - nobody will see your commits until you push your branch, so think of your branch as a scratch space. You have the ultimate freedom to use it however you want.

  • Branches are easy to create, so make liberal use of branches!

  • Be wary of force pushing, and of rewriting history.

Making Small Commits

Two essential git commands to help with making small commits are git add (patch mode) and git add (interactive mode).

git add patch mode

How to use:

git add -p <name-of-file>

The git add -p command allows the user to interactively stage individual changes made (in what is called patch mode). This means users can stage certain changes for one commit, then stage other changes for a different commit.

This solves the problem of making a long sequence of changes to a single file that should be logically separated into different steps. (For example, changing the import statements versus changing the name of a variable throughout a file).

For example, suppose we have the following changes to a file named doit.sh:

$ git diff doit.sh
diff --git a/doit.sh b/doit.sh
index 3b938a1..6c1aec8 100644
--- a/doit.sh
+++ b/doit.sh
@@ -1,6 +1,6 @@
 #!/bin/bash
 #
-# This script lists the 40 largest files in the git repo history
+# This script lists the 50 largest files in the git repo history

 $ git rev-list --all --objects | \
      sed -n $(git rev-list --objects --all | \
@@ -9,9 +9,9 @@ $ git rev-list --all --objects | \
      grep blob | \
      sort -n -k 3 | \
      \
-     tail -n40 | \
+     tail -n50 | \
      \
      while read hash type size; do
           echo -n "-e s/$hash/$size/p ";
      done) | \
-     sort -n -r -k1
+     sort -nru -k1

There are two related changes and one unrelated change, respectively: the two related changes are the change to the comment and the change to the tail command; the unrelated change is adding the -u flag to the sort command.

We can split these changes into two commits using git add -p doit.sh, which will walk through each change in the file and ask if we want to stage it:

$ git add -p doit.sh
diff --git a/doit.sh b/doit.sh
index 3b938a1..6c1aec8 100644
--- a/doit.sh
+++ b/doit.sh
@@ -1,6 +1,6 @@
 #!/bin/bash
 #
-# This script lists the 40 largest files in the git repo history
+# This script lists the 50 largest files in the git repo history

 $ git rev-list --all --objects | \
      sed -n $(git rev-list --objects --all | \
Stage this hunk [y,n,q,a,d,j,J,g,/,e,?]? y

@@ -9,9 +9,9 @@ $ git rev-list --all --objects | \
      grep blob | \
      sort -n -k 3 | \
      \
-     tail -n40 | \
+     tail -n50 | \
      \
      while read hash type size; do
Stage this hunk [y,n,q,a,d,j,J,g,/,e,?]? y

@@ -14,14 +14,14 @@        echo -n "-e s/$hash/$size/p ";
      done) | \
-     sort -n -r -k1
+     sort -nru -k1
Stage this hunk [y,n,q,a,d,j,J,g,/,e,?]? n

Now the two related changes are staged, and the unrelated change is not staged. This is reflected in git status:

$ git status
On branch master
Your branch is ahead of 'gh/master' by 2 commits.
  (use "git push" to publish your local commits)

Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
    modified:   doit.sh

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
    modified:   doit.sh

Now git commit will commit only the staged portions.

Do not provide any filenames to git commit, so that git will only commit the staged changes.

To use this in your workflow, think about how you can group different changes together into different commits. If you get a portion of a feature working, you can commit the changes in groups so that related changes get committed together.

Also remember that if your commit history ends up being excessively long or overly detailed, you can always examine what changes different commits made with git diff, and reorder them with git cherry-pick or modify/combine them with git rebase.

git add editor mode

How to use:

git add -e <name-of-file>

Like the interactive patch mode, git add -e allows you to selectively stage certain changes in a file. But it's much better for keyboard jockeys and those that love their text editor, because you can choose which changes to stage or not using the text editor.

A sidebar:

If you have not yet set the text editor that git uses, you should do that now. Modify your git configuration with this command:

git config --global core.editor vim

Alternatively, put the following in your ~/.gitconfig:

[core]
    editor = vim

(Or, you know, whatever your text editor of choice is.)

End of sidebar.

When you pass the -e flag to git add, it will open a new editor window with the full diff:

diff --git a/doit.sh b/doit.sh
index 326273c..14e4059 100644
--- a/doit.sh
+++ b/doit.sh
@@ -1,17 +1,17 @@
 #!/bin/bash
 #
-# This script lists the 50 largest files in the git repo history
+# This script lists the 10 largest files in the git repo history

 $ git rev-list --all --objects | \
      sed -n $(git rev-list --objects --all | \
      cut -f1 -d' ' | \
      git cat-file --batch-check | \
      grep blob | \
      sort -n -k 3 | \
      \
-     tail -n50 | \
+     tail -n10 | \
      \
      while read hash type size; do
           echo -n "-e s/$hash/$size/p ";
      done) | \
-     sort -nru -k1
+     sort -nr -k1

Editing this file requires some care!

Fortunately there is a section in the documentation for git add called Editing Patches.

Two things to remember:

  • Lines starting with + indicate new, added content. To prevent this content from being added, delete the line.

  • Lines starting with - indicate removed content. To keep this content, replace - with a space ().

Once you are finished, make sure you review the changes that are staged, particularly if this is the first time seeing patch files or the diff syntax.

Modifying Commits

There is always some reason or another to modify the commit history of a repository - perhaps someone's work was lost, or the wrong issue or pull request number was referenced, or a username was misspelled.

You can always modify a commit, but it will also modify every commit that came after it. Think of it like replaying the changes recorded in each commit onto the new branch. The contents of each commit changes slightly, so the hash (the name) of every commit changes.

git rebase

To do a git rebase, an interactive rebase (the -i flag) is recommended.

The rebase action takes two commits, and will replay the commits.

IMPORTANT: The first commit given (the start commit) is not included in the rebase. To include it, add ~1 to the start commit. (For example, 0a1b2c3d~1 refers to the commit before commit 0a1b2c3d.

rebasing a range of commits

To rebase from the start commit hash to the end commit hash, and include the start commit in the rebase, the rebase command is:

git rebase -i START_COMMIT_HASH~1 END_COMMIT_HASH

This does not indicate a destination branch. The default behavior is for the branch to move and the new pile of commits to retain the same branch name.

rebasing onto another branch

To rebase a range of commits onto a different branch (for example, onto a master branch that has the latest changes from the remote), use the --onto flag:

git rebase -i START_COMMIT_HASH END_COMMIT_HASH --onto TARGET_BRANCH

IMPORTANT: The above rebase commands will leave your repo in a headless state - unlike the behavior of the prior command, the branch label will not move with you to the new pile of commits.

Run git checkout -b <branchname> to give your new rebased branch a meaningful name. This creates a branch wherever HEAD is, which is pointing to the top of the pile of rebased commits.

If you want the old branch label to move to the new pile of commits, it requires a bit of branch housekeeping - you have to delete the old branch, then create a new branch from where HEAD is (the end of the rebase), then check out that branch.

git branch -D <branchname> && git checkout -b <branchname>

Rearranging Commits

Where rebasing allows for editing commits en masse, cherry picking allows the changes made in individual commits to be applied anywhere - including other branches. This makes the atomic commit principle from the beginning of this post much easier - groups of related commits that happened out of order can be rearranged by cherry picking them onto a new branch, and the new branch is a better "story".

Combining Commits

The cherry pick operation can also be combined with a rebase - once multiple small commits are arranged together chronologically, a git rebase operation enables squashing those tiny commits into a small number of larger commits, all carrying related changes.

Tags:    git    rebase    cherry-pick    branching    version control   

Git Workflows, Part 1: Supercharging your Git Config

Posted in Git

permalink

Source

Most of the good stuff is from https://github.com/mathiasbynens/dotfiles!

User Section

Start off easy - here's how you set your email and name for commits:

[user]
    email = foo@bar.com
    name = Foo Bar

Bash Aliases

The Best One Letter Alias Ever

Start supercharging how you use git by creating a one-letter alias.

Add this to your ~/.bashrc file:

alias g="git"

You're already saving yourself a bunch of keystrokes, and we're just getting started!

Ending Bad Habits

This is a nice trick for getting yourself out of bad habits. My first time using a "sophisticated" branch worklow in git (i.e., not just committing and pushing to master all the time), I got in trouble for committing directly to master with a git push origin master (instead of making a feature branch and opening a pull request).

To get myself out of the habit of typing git push origin master, I wanted to map it to an alias that told me no. I did that by defining git to be a bash function (this works because functions take precedence over a binary named git on your path).

The git function checks the arguments that are passed to it. If the arguments are push origin master, it means I'm typing git push origin master, and I get a slap on the wrist.

Otherwise, it passes the arguments through to the git binary.

You can also put this in ~/.bashrc.

git() {
    if [[ $@ == "push origin master" ]]; then
        echo "nope"
    else
        command git "$@"
    fi
}

Alias section

In the ~/.gitconfig file, aliases specific to git can be defined in a section beginning with alias.

Log Utils

Let's start with some utilities for viewing git logs.

(You can never have too many ways to look at a git log.)

Note that we'll assume the alias bit in the following git config excerpts.

[alias]
    # courtesy of https://stackoverflow.com/a/34467298
    lg = !"git lg1"
    lg1 = !"git lg1-specific --all"
    lg2 = !"git lg2-specific --all"
    lg3 = !"git lg3-specific --all"

    lg1-specific = log --graph --abbrev-commit --decorate --format=format:'%C(bold blue)%h%C(reset) - %C(bold green)(%ar)%C(reset) %C(white)%s%C(reset) %C(dim white)- %an%C(reset)%C(auto)%d%C(reset)'
    lg2-specific = log --graph --abbrev-commit --decorate --format=format:'%C(bold blue)%h%C(reset) - %C(bold cyan)%aD%C(reset) %C(bold green)(%ar)%C(reset)%C(auto)%d%C(reset)%n''          %C(white)%s%C(reset) %C(dim white)- %an%C(reset)'
    lg3-specific = log --graph --abbrev-commit --decorate --format=format:'%C(bold blue)%h%C(reset) - %C(bold cyan)%aD%C(reset) %C(bold green)(%ar)%C(reset) %C(bold cyan)(committed: %cD)%C(reset) %C(auto)%d%C(reset)%n''          %C(white)%s%C(reset)%n''          %C(dim white)- %an <%ae> %C(reset) %C(dim white)(committer: %cn <%ce>)%C(reset)'

The git lgX shortcuts give similar views of the log, but with increasing vertical spacing. git lg1 is the most compact, while git lg3 is the most comfortable to read, as far as vertical whitespace. Same with the -specific commands.

This is one more nice short log command:

    # View abbreviated SHA, description, and history graph of the latest 20 commits
    l = log --pretty=oneline -n 20 --graph --abbrev-commit

Remember to use this with the g alias for super short log:

$ g l
* 4357b28 (HEAD -> source) update mocking aws post
* 063ad78 (gh/source, gh/HEAD) add mocking post
* a5f1adc add init keras cnn post
* fb911ec add keras cnn draft
* 3549d35 add rosalind (euler paths) part 7 draft

$ g lg1
* 4357b28 - (67 minutes ago) update mocking aws post - Charles Reid (HEAD -> source)
* 063ad78 - (2 weeks ago) add mocking post - Charles Reid (gh/source, gh/HEAD)
* a5f1adc - (4 months ago) add init keras cnn post - C Reid
* fb911ec - (5 months ago) add keras cnn draft - C Reid
* 3549d35 - (5 months ago) add rosalind (euler paths) part 7 draft - C Reid
...

Status Utils

The git status command is one of my most frequently used commands, so I made a few shortcuts:

    # View the current working tree status using the short format
    s = status -s
    ss = status

This makes checking the short or long status of a git repo easy:

$ g s
AM pelican/content/git-workflows-1-config.md
AM pelican/content/git-workflows-2-teams.md

$ g ss
On branch source
Your branch is ahead of 'gh/source' by 1 commit.
  (use "git push" to publish your local commits)

Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
    new file:   pelican/content/git-workflows-1-config.md
    new file:   pelican/content/git-workflows-2-teams.md

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
    modified:   pelican/content/git-workflows-1-config.md
    modified:   pelican/content/git-workflows-2-teams.md

Fetching

Fetching is handy to do, since it just fetches changes from a remote and doesn't actually change anything or try to merge anything (unlike a git pull command).

The most useful fetch command (git fetch --all) is aliased to g f with the following bit in the aliases section of the ~/.gitconfig file:

    f = fetch --all

Branch Utils

The only command I might use more than the status command are branch commands, so here are several branch aliases:

    b = branch -v
    bv = branch -v
    bb = branch -v

    ba = branch -a
    bb = branch -v -a

In a similar way, you can get a summary view using g b:

$ g b
  master 4c828cd [behind 84] update with awsome day notes
* source b18adfd add two git workflow posts

$ g b
  master 940ee98 update mocking post
* source b18adfd add two git workflow posts

and a little bit more information with g bb:

$ g bb
  master            940ee98 update mocking post
* source            b18adfd add two git workflow posts
  remotes/gh/HEAD   -> gh/source
  remotes/gh/master 940ee98 update mocking post
  remotes/gh/source b18adfd add two git workflow posts

Branch and Checkout

Sometimes if you are creaing a branch with a long branch name, it can be inconvenient to have to first create the branch with git branch <branch-name> and then check it out with git checkout <branch-name>.

To resolve this you can define a git go alias that creates the branch and then switches to that branch:

    # Switch to a branch, creating it
    # from the current branch if necessary
    go = "!f() { git checkout -b \"$1\" 2> /dev/null || git checkout \"$1\"; }; f"

Careful you don't mistype the branch name.

Remote Utils

Another useful git command is the remote command, so here are a few remote aliases:

    r = remote -v
    rv = remote -v
    ra = remote -v

Commit Utils

Sometimes you have changes that you've staged using git add, but you want to see the changes that you've staged, before you commit them.

Normally you'd have to use the inconvenient git diff --cached <files>, but this can be aliased to cdiff, so that you can use git diff to see unstaged changes and git cdiff to see staged changes.

Even better, you can define the alias g cd to run git cdiff...!

Here's the relevant bit in the aliases section:

    cdiff = diff --cached
    cd = diff --cached

Committing All Changes

    # Commit all changes
    ca = !git add -A && git commit -av

Fixing Commits

Some common operations for repairing commit history before pushing:

    # Amend the currently staged files to the latest commit
    amend = commit --amend --reuse-message=HEAD

    # Oops
    fix = commit --amend --reuse-message=HEAD --edit

Miscellaneous Utils

There are a few other actions that are useful to add to the aliases section of the ~/.gitconfig:

Rebasing shortcuts

    # Interactive rebase with the given number of latest commits
    reb = "!r() { git rebase -i HEAD~$1; }; r"

Diff shortcuts

    # Show the diff between the latest commit and the current state
    d = !"git diff-index --quiet HEAD -- || clear; git --no-pager diff --patch-with-stat"

    # `git di $number` shows the diff between the state `$number` revisions ago and the current state
    di = !"d() { git diff --patch-with-stat HEAD~$1; }; git diff-index --quiet HEAD -- || clear; d"

Pull shortcuts

    p = "!f() { git pull $1 $2; }; f"

Clone shortcuts

    # Clone a repository including all submodules
    c = clone --recursive

Contributor shortcuts

This last one is convenient for getting a summary of contributors:

    # List contributors with number of commits
    contributors = shortlog --summary --numbered

An example for https://github.com/aws/chalice:

$ cd chalice/
$ g contributors
  1053  James Saryerwinnie
   120  John Carlyle
    94  stealthycoin
    42  kyleknap
    35  jcarlyl
    19  Kyle Knapp
    12  Atharva Chauthaiwale

Core section

Because it's the best text editor:

[core]
    editor = vim

I have some other stuff I've collected, many of them from https://github.com/mathiasbynens/dotfiles:

    # Use custom `.gitignore` and `.gitattributes`
    excludesfile = ~/.gitignore
    attributesfile = ~/.gitattributes

    # Treat spaces before tabs and all kinds of trailing whitespace as an error
    # [default] trailing-space: looks for spaces at the end of a line
    # [default] space-before-tab: looks for spaces before tabs at the beginning of a line
    whitespace = space-before-tab,-indent-with-non-tab,trailing-space

    # Make `git rebase` safer on macOS
    # More info: <http://www.git-tower.com/blog/make-git-rebase-safe-on-osx/>
    ###trustctime = false

    # Prevent showing files whose names contain non-ASCII symbols as unversioned.
    # http://michael-kuehnel.de/git/2014/11/21/git-mac-osx-and-german-umlaute.html
    precomposeunicode = false

    # Speed up commands involving untracked files such as `git status`.
    # https://git-scm.com/docs/git-update-index#_untracked_cache
    untrackedCache = true

Color section

Make some nice beautiful colors that are easy to understand:

[color]

    # Use colors in Git commands that are capable of colored output when
    # outputting to the terminal. (This is the default setting in Git ≥ 1.8.4.)
    ui = auto

[color "branch"]

    current = yellow reverse
    local = yellow
    remote = green

[color "diff"]

    meta = yellow bold
    frag = magenta bold # line info
    old = red # deletions
    new = green # additions

[color "status"]

    added = yellow
    changed = green
    untracked = cyan

Url section

This makes some Github-related URLs easier and shorter to type:

[url "git@github.com:"]

    insteadOf = "gh:"
    pushInsteadOf = "github:"
    pushInsteadOf = "git://github.com/"

[url "git@gist.github.com:"]

    insteadOf = "gst:"
    pushInsteadOf = "gist:"
    pushInsteadOf = "git://gist.github.com/"

[url "git://gist.github.com/"]

    insteadOf = "gist:"

Now, instead of

$ git clone git@github.com:org-name/repo-name

you can do the much simpler

$ g c gh://org-name/repo-name

Voila! Start integrating these alises into your daily workflow, and you'll find yourself using a lot fewer keystrokes!

Tags:    git    rebase    cherry-pick    branching    version control   

Mocking AWS in Unit Tests

Posted in Python

permalink

Overview

This post covers a technique for mocking AWS in unit tests so that you can test functionality that normally requires API calls and handling responses, by mocking those responses instead of making actual API calls.

A Simple Example: Mocking API Responses

The Genuine AWS Call

Let's start with an example of an AWS API call. Here's how our program will be structured: start with a driver lister.py that creates an AWS secrets manager client and defines a function to list secrets using the secrets manager client, then a test for it in test_lister.py that mocks the AWS call.

This example is simple and uses just one function, list_secrets(), which returns a JSON response that looks something like this:

{
  "SecretList": [
    {
      "ARN": "arn:aws:secretsmanager:us-east-1:000000000000:secret:prefix/secret1-abc123",
      "Name": "prefix/es_source_ip",
      "LastChangedDate": "2019-09-23 17:29:16.267000-07:00",
      "LastAccessedDate": "2019-09-23 17:00:00-07:00",
      "SecretVersionsToStages": {
        "658c3b41-0806-48b9-b05d-ea7dc2dbf237": [
          "AWSCURRENT"
        ],
        "f37ccfe2-16e0-4305-a250-ef89d2c47ece": [
          "AWSPREVIOUS"
        ]
      }
    },
    {
      "ARN": "arn:aws:secretsmanager:us-east-1:000000000000:secret:prefix/secret2-def789",
      "Name": "prefix/secret2",
      "LastChangedDate": "2019-09-22 17:05:01.431000-07:00",
      "LastAccessedDate": "2019-09-22 17:00:00-07:00",
      "SecretVersionsToStages": {
        "95AE5F8B-34E7-4EDF-A672-9E3AF1A4732E": [
          "AWSCURRENT"
        ],
        "F29E224A-BC03-4780-B64E-EA666B99D952": [
          "AWSPREVIOUS"
        ]
      }
    }
  ]
}

Using the secrets manager API:

lister.py:

import boto3

sm_client = boto3.client('secretsmanager')

def print_secret_names():
    s = sm_client.list_secrets()
    for secret in s['SecretList']:
        if 'Name' in secret and 'LastAccessedDate' in secret:
            print(f"Secret Name: {secret['Name']} (last accessed: {secret['LastAccessedDate']})")

if __name__=="__main__":
    print_secret_names()

If we run this file, we'll see a list of secrets in the real secrets manager - that is, the secrets manager that is linked to the boto credentials in ~/.aws, so the secrets we see are the actual secrets in the secret manager:

$ python lister.py
Secret Name: prefix/secret1 (last accessed: 2019-09-23 17:00:00-07:00)
Secret Name: prefix/secret2 (last accessed: 2019-09-23 17:00:00-07:00)
Secret Name: prefix/secret3 (last accessed: 2019-09-23 17:00:00-07:00)

The Mocked AWS Call

It is important to only mock the functionality we need. We should mock the returned JSON, but only the Name and LastAccessedDate fields.

To mock the call to list_secrets(), we start by importing mock from unittest. Then we import the file that has the function we want to test. We also import any other modules we need.

Next, we are mocking a call to a method of an object, which we can do by creating a context via with mock.patch() (and passing it a string with the name of the object we want to mock, or patch).

import unittest
from unittest import mock
import lister
import datetime

class TestMo(unittest.TestCase):
    def test_main(self):
        with mock.patch("mo.sm_client") as sm:
            ...
            sm_client.list_secrets = mock.MagicMock( ... )
            ...

Any calls made to sm_client in the mo module will be mocked using the mock.MagicMock object that we define in the context, so we craft the response we want before we call the method we want to test (which in turn will call sm_client.list_secrets()).

The full version of the test looks like this:

test_lister.py:

import unittest
from unittest import mock
import lister
import datetime

class TestLister(unittest.TestCase):
    def test_main(self):
        with mock.patch("lister.sm_client") as sm:
            return_json = {
                "SecretList": [
                    {
                        "Name": "fakesecret1",
                        "LastAccessedDate": datetime.datetime.now()
                    },
                    {
                        "Name": "fakesecret2",
                        "LastAccessedDate": datetime.datetime.now()
                    }
                ]
            }
            sm.list_secrets = mock.MagicMock(return_value = return_json)
            lister.print_secret_names()

if __name__=="__main__":
    unittest.main()

When the test file is run via Python, we see the fake secrets:

$ python test_lister.py
Secret Name: fakesecret1 (last accessed: 2019-09-23 20:31:49.186874)
Secret Name: fakesecret2 (last accessed: 2019-09-23 20:31:49.186880)
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

Tags:    python    pytest    tests    aws    mock    mocking   

March 2022

How to Read Ulysses

July 2020

Applied Gitflow

September 2019

Mocking AWS in Unit Tests

May 2018

Current Projects