Slack Status Updater (CLI) - Changing a status
In this one, I use the model created and pass it into an API wrapper function. And it works! At the end of it, I tie it together as an experiment by importing the core into the CLI project and making an API call from there!
February 7th 2018
Going into this part I instinctively know I'm going to have some challenges. Here's how I would usually architect something like this:
- Create an object of type
Model
- Pass the
Model
type object to anAPI_Wrapper
- The
API_Wrapper
reads the properties of theModel
and correctly formats them if needed and sends it to theReal_API_Library
.
If this seems a little contrived, it might be for this case. But I've found that in many cases when using an API Library, there are some things I might want to do differently than the API library does. For example, if I was using an API to read tasks from task app into a custom app of mine, I might want to enforce logic of not deleting a task through my application if the task's status is 'Complete'. In that case, my API wrapper for the delete method will call on all the necessary methods in the Real_API_Library
to follow this logic.
Additionally, I've been bitten in the past where after a couple of years, the library loses support if it's a 3rd party maintained one. This happens mostly when the API provider releases a new major version of their API spec. At that point one has to hope that the library maintainer will adopt it, or we'd need to fork the project and do that. The latter option has rarely made sense because I wouldn't be using all the methods specified in the library and so there's not much sense in keeping that around. Better to swap it out either for my own implementation or a newer one that the community might have developed.
When using this approach of a wrapper, that can be as simple as changing an import. On occasion it can be as complex as rewriting logic partially to support a new library's new way of calling methods. BUT! It's only in one place :)
But I digress. The real issue that I am going to face with this, is testing. In the past what I would do would be to mock the API library if I was using a language like Python. If I was using something like Java or anything that doesn't allow mocking like this, then I'd pass the library (or instance of the library) into the function. Then when it came to testing, I'd ideally inherit from the library and pass a mock implementation of some interface.
I don't know how to do any of this in golang
. So it's time to roll up sleeves and figure it out.
February 8th 2018
09:40
Before jumping into mocks, I need to grab the library that I want to use. Ideally write some code that interfaces with it so I can get to know how this works. After that hopefully I can jump into making the tests and mocks happen.
- Dug through the notes to remind myself how this goes. The Slack SDK is here, and the
deps
tool documentation is here.
09:52
I forgot how to get packages. go get github.com/nlopes/slack
worked for me. Installed another dependency as well. Honestly, this SDK does everything when all I want is access to a few methods via the basic web API.
Aaaand power outages today. Seeing that my productivity was absolutely ruined here, I'm going to switch over to Buffer work and try and crank some stuff out there heavily and come back to this tomorrow early morning.
March 21st 2018
It's been a bit of a train wreck for the past month and half so I didn't focus on my side projects at all. I was just trying to keep getting my normal work done through the stress of things and that meant that tasks that would take me 2 hours were taking me almost twice as long. But hopefully it's finally over and I'm going to try and get back into this. Right now, the motivation is kinda zero and I have to figure out where to start from all over again. But let's give this a shot.
08:55
First things, I have to figure out where I stopped. Thank goodness for notes. And I'm going to re read code and notes because I can't remember squat.
- So it looks like I have already installed the package on this machine. That's good.
09:01
I think I can jump in. I have enough to go by. What I want:
- Create a wrapper to call the SDK. This wrapper will only have the methods to set a status for now.
- Create a mock of the wrapper that is used for testing.
- Check if the mock has been called when testing.
I'm pretty confident of the first two of those points. I have less hopes about the last one.
09:16
After reading old notes I think I want to try something first. I just realised I haven't created any methods that follow the format of func (s *Status) setStatus() err {}
, aka Method declarations (you can see my investigations of that quirk here
09:35
More refreshing of memory. I have my first failing function that calls a method that doesn't exist yet and expect a nil
value for err
to be returned:
func TestSetStatusMethodIsCalled(t *testing.T){
s := Status{"lunch", "chompy", "Having lunch"}
err := s.setMyStatus()
if err != nil {
t.Fatalf("Error was not nil. Received: %s", err.Error())
}
}
And the corresponding function to pass things.
func (s *Status) setMyStatus() (error) {
return nil
}
This is great. From here I need to create my mock API wrapper and have it be called in this method.
10:03
Pausing here. But what I've decided is that for the moment, since I only need one function (setStatus
or something like that), I can actually pass a function in as a variable to the someStatusVar.setMyStatus
method. Which seems really easy to do and probably what I shall do. I still have no idea to see if it was called or not.
11:40
While driving home I suddenly thought of a possible idea. What if I could read Python's unittest.Mock
implementation. I know that they have a method to see if a mocked/patched method has been called. Maybe I can find some clues there. But first, I should do some googling and see what's already available out there.
- Honestly, it seems like
testify
is pretty robust. Just that the ownership around the project seems to be a little doubtful for the long run. Saying that with all due respect towards the core maintainer of the project. He has zero obligations towards any of us. Wish that the golang team would adopt it as an officially supported library. Either way, I'll work around needing all of that for now by returning a value and testing it.
12:30
After some reading on how to do this and some experimentation, I have it!
//In a new file called apiwrapper.go that belongs to ssucore package
type UpdateStatus func(*Status) (string, error)
func UpdateStatusViaSDK(s *Status) (string, error) {
return "something", nil
}
//From the setstatus_test.go file
func updateStatusViaAPIMock(s *Status) (string, error){
return "Status was successfully changed", nil
}
func TestSetStatusMethodIsCalled(t *testing.T){
s := Status{"lunch", "chompy", "Having lunch"}
result, err := s.setMyStatus(updateStatusViaAPIMock)
if err != nil {
t.Fatalf("Error was not nil. Received: %s", err.Error())
}
if result != "Status was successfully changed" {
t.Fatalf("Result message was not the expected value. Received: %s", result)
}
}
//from setstatus.go
func (s *Status) setMyStatus(fn UpdateStatus) (string, error) {
return fn(s)
}
12:41
Right then. I added some if conditions to my mock function to check that the function was called with the right values as well. I see no reason to delay making a commit and writing code that calls the actual API now :).
12:53
I spend a mad amount of time reading docs. Just re-read the deps
documentation. I didn't realise there was something called dep ensure -add <package-to-install>
command that would install the library and update the lock
and toml
files. Just ran that. Now reading the nlopes/slack
package documentation.
- Looks like I'll add a new parameter to the function to pass the API key in. Change is here. The reason for passing the parameter in instead of having the core read a file from somewhere is that the core should not be concerned with the specifics of the system. The CLI cares where the config is stored. It'll probably be in
~/.config/ssucli/config
. But if it was a web app? It'd be looking for the value in an environment variable probably. It's the CLI's responsibility to find that value and pass it in to the core.
March 22nd 2018
I honestly thought I hit save on the post after typing a bit after the last bullet point. But I didn't and I really need to remember this. Anyways, I basically stopped there for the day after reading the docs around the SDK. Today I'm going to be actually connecting to the SDK and trying to run it. I'll be adding a test to see if the API wrapper actually works, and I'll be using an environment variable to pass the token into a function call. A few things I want to check for today:
- Testing the SDK will be a slow test. I want to see how I can run only certain tests. Maybe it means commenting it out. I don't know.
- The API uses a mechansim called a token. The thing is, the idea of a personal token is called a Legacy token in Slack. The term legacy is usually associated with being up for deprecation. I'd love to know if that's the case. Hopefully not.
- Does the legacy token actually grant me permission to change my user profile? Only one way to find out.
09:39
So I've updated the function UpdateStatusViaSDK(s *Status) (string, error)
to have an actual body that makes the call to the api using the SDK. It'll create the api
variable and call the method, check for an error, and do the usual dance. Now for the test (I'm writing this in reverse yes) I need to get the SLACKTOKEN
from the environment variable and pass it in to the UpdateStatusViaSDK
function.
- Simple enough. Import the
os
package and useos.Getenv("ENV_NAME")
to retrieve.
09:45
Now how do I run only the test I want?
- Well. Looks simple enough. Just use the
-run
flag with the name of the test I want to run. - Ha we have success!
09:51
Some interesting learnings while going about this.
- I used
fmt
to create the formatted string I wanted to return from the function. It turns out thatfmt.Printf
doesn't actually return a string. It returns anint
anderror
. If you want to get a formatted string returned, you usefmt.Sprintf
to make it happen. - I find golang's choice of passing a copy of a variable into the function as the default mechnism to be maddening. Basically, if I write a function as:
func someFunc(a MyStruct){
//blabla
}
that will receive a "copy" of the parameter I pass into the function. So any edits I make on a
will not actually reflect outside of the function. Which is fine. But in Go's official docs, they say it's better to pass as reference (using a pointer) because that's more memory efficient esp when using large objects.
func someFunc(a *MyStruct){
//blabla
}
//and call it using
someFunc(&myInstanceOfMyStruct)
But if it is the recommended way of doing it, why not do it by default??
09:57
Breaking for the morning. Final commit of changes is here
17:39
So now that the core is "done", I can get to work on the CLI. I'll be doing that tomorrow, but before I end my work for the day, I did want to try something out so I don't have to start on it tomorrow. I want to know how to import my core package into the CLI.
17:44
The first thing I did was to create this super basic cli_test.go
file in the CLI folder.
package main
import (
"testing"
)
func TestBasic(t *testing.T) {
if "a" != "a" {
t.Fatalf("oh noes")
}
}
And then I ran dep ensure -add github.com/kiriappeee/slack-status-updater-core
. Now to figure out how to import it and call the API method.
17:48
HA! That was easy. I added github.com/kiriappeee/slack-status-updater-core
into the imports. Then I can access everything as ssucore.<whichever-exported-thing-I-need
. So in this cast I created a Status object using s := ssucore.Status { ... }
17:51
Aaaand ssucore.UpdateStatusViaSDK(&s, os.Getenv("SLACKTOKEN"))
worked :) . I just changed my status. Tomorrow will start off as a new post then. All about completing the CLI itself. Woohoo!
This post is part of a series of logs exploring how I'm building a CLI for updating my slack status. You can follow the full project here
Posted on March 22 2018 by Adnan Issadeen