Ruby, etc

Learn all the things

He who enjoys doing and enjoys what he has done is happy. - Fortune Cookie

Automation - What to Test?

Recently I’ve been working with a QA team and I’ve learned how to test the browser from them and have came up with some of my own ideas learning from Trial and Error. I have spent close to 1.5 years doing Automation Testing whereas previously I’ve always been a test obsessed developer with little in the way of browser testing.

This post is focusing on testing the browser and automating the tests.

I’ve already wrote about how I setup Robot Framework.

But what to test?

Here’s some questions to think about:

What button or link on my page makes money either directly or indirectly? It could be a Buy Now button or a subscription to a newsletter. Those are candidates to test.

Which thing on the page which didn’t work, would make us look stupid? Ok you want to have some fancy dropdown that doesn’t look like an HTML dropdown. But then it doesn’t work. As a user, I would probably be so mad I would leave the site. I don’t really have time to waste on stupid sites.

Things are that easy to test – why not? Contact us link, about us, FAQ. Make sure those links are going to the right place. 5 minutes of your time gives you the confidence your site is functioning. Test that there is a valid url and when you go to it something shows up to prove you are on the right page, wether a H1 or page title.

This is easy, test your copyright date if you have it :) 2018 is over half over and I’m still seeing current sites with 2017. Use some scripts to get the current year and check it.

Test the navigation the user might use, make sure each link is properly labeled and goes to the right page.

What do you hate testing Manually?

If it is a complex work flow of choosing multiple options and certain buttons to get to a certain page. That is tedious and should be automated. Not only do you make it “repeatable” but you won’t forget steps later when you haven’t worked on the site for some time.

What not to test?

Things that don’t break often. Sorting a list alphabetically? Probably not going to break. Computers are good at that. If you insist on testing this on the frontend a good way to do it is to get all the items in a collection and sort it , then compare the order to what is on the site. Sorting by date is another thing that probably can be covered in unit tests and not worth testing. Remember we are trying to test as a user.

Things already implemented in unit tests. Most of the time you don’t want to double test. For things like cookies/security tokens, etc you do want to double test on both frontend and backend. Just to be sure.

Use Jenkins

Setup Jenkins to run your tests. I wrote about it. If you did all this work to write them set them up to run and emails you on failures or ping a chat room or something. Jenkins makes it really easy.

Write a test you want to maintain in future. If you are doing something insanely complex and have a good reason, comment it to save some stress for the next guy working on your tests. Or better yet, just write tests as simply as you can. Err on the side of maintainability instead of cleverness. Your future self will thank you.

Use IDs or Descriptive Class Names

For the Love of Coding, please .. if your Devs won’t give descriptive IDs (ie nav, main, sidebar etc) then at least get them to add some class names that describe what it is and not “how” .. a “how” classname might be text-left. Sure thats great for your designer, but that classname doesn’t help the QA person.

If adding IDs or Classnames won’t get you what you need, ask them to add custom attributes (allowed now in react 16).

Conclusion

Browser testing is frustrating but it is a much needed type of testing that everyone should have at least in some aspect. Ignoring these tests because they are a PITA is not a great idea. I hope I’ve given you some ideas on how you might start implementing it on your own sites.

My Robot Framework Setup

I’ve been using Robot Framework for a little over a year now. I have a handful of commits between Robot Framework (RF) and the RF Selenium Library and I’m active in the Robot Slack Group. It is a really nifty test automation framework.

This is a little bit about how my tests are arranged and how I setup a new project.

Sample structure:

1
2
3
4
5
6
7
8
9
10
11
12
├── README.md
└── suites
    ├── api
    │   ├── api_common.robot
    │   ├── product.robot
    │   └── user.robot
    ├── common.robot
    └── website
        ├── homepage.robot
        ├── login.robot
        ├── search.robot
        └── website_common.robot

Each robot file is a test suite and contain the tests. The “common” files contain my settings and keywords.

Now here’s what is in the main “common” file:

in my main common file I have the following:

suites/common.robot

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
*** Variables ***
${STACK}  local
${SCREEN}  desktop
${BROWSER}  chrome

${BASE_URL}=   https://www.myawesomepage.com

*** Keywords ***
Open Browser Stack
    [Arguments]    ${URL}
    Open Browser   url=${URL}   browser=${BROWSER}   remote_url=${RemoteUrl}   desired_capabilities=browser:${BROWSER},os:${OS}

Open Test Browser
    [Arguments]    ${URL}
    # default as defined at top of file is 'local'
    Run Keyword If  "${STACK}" == "browserstack"  Open Browser Stack     ${URL}
    Run Keyword If  "${STACK}" == "local"         Open Browser  ${URL}  ${BROWSER}

This also makes it easier to run on browser stack by passing along a variable “-v STACK:browserstack”. By default it runs “local”.

Why wrap the Open Browser keyword in another keyword? I’ll show you why in a bit. This is slimmed down for this blog post :)

Notice the variables at the top, they are the “defaults” that are used unless you override them with a command line argument.

Ok so once I have my file structure setup, and my common files setup: Here’s my first test.

Why set the url of the page in a variable? This makes it easy to pass in a staging or qa url there if you have multiple sites. If you don’t, then you have a easy to do that in future.

suites/website/homepage.robot

1
2
3
4
5
6
7
8
9
*** Settings ***
Documentation  My Amazing Homepage test
Resource       website_common.robot
Test Setup     Open Test Browser  ${BASE_URL}
Test Teardown  Close All Browsers

*** Test Cases ***
Ensure Header Appears
   Page Should Contain Element   css=.header

Then for my search page (notice I was able to use the $BASE_URL to specify the url):

suites/website/search.robot

1
2
3
4
5
6
7
8
9
10
11
12
*** Variables ***
${SEARCH_URL}=  ${BASE_URL}/search

*** Settings ***
Documentation  My Amazing Search test
Resource       website_common.robot
Test Setup     Open Test Browser  ${SEARCH_URL}
Test Teardown  Close All Browsers

*** Test Cases ***
Ensure Search Appears
   Page Should Contain Element   css=.search-box

I said I had more reasons to wrap Open Browser .. sometimes you might want to do something on each test like Log the current browser version or resize the window.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Set Window Desktop
   Set Window Size   1280  1024

Set Window Tablet
   Set Window Size    768   1024

Set Window Mobile
   Set Window Size    320   568

Set Test Browser Size
    Run Keyword If  "${SCREEN}" == "desktop"  Set Window Desktop
    Run Keyword If  "${SCREEN}" == "tablet"   Set Window Tablet
    Run Keyword If  "${SCREEN}" == "mobile"   Set Window Mobile
    Run Keyword If  "${SCREEN}" == "max"      Maximize Browser Window
    Set Suite Metadata    Browser Size    ${SCREEN}

Log Browser Version
    # the default log level logs the evaluation of this command in the report
    ${browser_version}=   Execute Javascript    return navigator.userAgent
    Set Suite Metadata    Browser Version    ${browser_version}

See these keywords I have defined to do those tasks, and if you want them run each time you can put them in your Open Test Browser keyword like this:

1
2
3
4
5
6
7
Open Test Browser
    [Arguments]    ${URL}
    # default as defined at top of file is 'local'
    Run Keyword If  "${STACK}" == "browserstack"  Open Browser Stack     ${URL}
    Run Keyword If  "${STACK}" == "local"         Open Browser  ${URL}  ${BROWSER}
    Log Browser Version
    Set Test Browser Size

You can run your tests for different size browsers now by passing in the variable for screen, like this -v SCREEN:mobile.

Putting the browser in the test report helps when two people run the test and get different results! You could have each person check their version of the browser but its nice to have it in the report.

Thats how I setup my robot tests, let me know if you see anything that I can do better :)

Setting Up Jenkins for Robot Framework

Robot framework is a test automation framework. There are many uses for it, but I use it for selenium web testing. But no matter what you test you can use it for many things.

Jenkins a tool to manage “jobs”, segments of work that you might use “cron” for.

I use it to run robot framework tests nightly or every 6 hours, or whatever you need.

Jenkins is breeze to set up (at least on mac) and once installed search in the plugins for the robot framework plugin. The directions on the plugin page are good but. However, If you wish to view the report you have to enable Javascript on jenkins.

There have been lots of questions on this subject but it all comes down to jenkins permissions.

To view the beautiful reports by robot framework you must relax the ridged rules explained here. This new feature was added in Jenkins 1.641.

Once JS is enabled then you can view the report and drill down on areas you want to see, including screenshots.

Another jenkins plugin I’ve found useful is Naginator to restart a job after a failure.

Once the Jenkins plugin is installed, configure your robot command line:

Build Step

And then your results:

Post Build

Contributing to Open Source

You probably use open source software if you are programmer and you know you should probably at some point give back right? I’ve been programming since I was a kid and working as a professional programmer for 17 years (at the time of this post). I freaking love code but I wouldn’t say I’ve contributed a lot in the past years. :(

I want to talk about some of my experiences and what you can do to get started.

I contributed to Clojure 1.8 by adding more functions to the string namespace. I took off two weeks from consulting for “vacation” and worked on that. I found this particular task by posting on twitter asking for any ideas for a project I could complete in a short amount of time. Alex Miller saw it and said he had a ticket I could work on and he would help me. It was a great experience and I learned a lot. The Clojure Team has a system of applying patches so they don’t use the typical “github pull requests” but it was an easy enough process to learn and it was fine.

Most open source projects on Github use pull requests. I remember one of my first pull requests to a project was to organize the examples at httparty (ruby). At the time I was using a lot of httparty in my project and kept going back to the examples and always forgetting which one was which. Organizing it make it easy to see which examples did what.

My current company Condé Nast has been holding Open Source Hack Nights, where we meet as a company and work on project that helps us with our job. I have been working with Robot Framework a test automation tool. This was the push I needed. There have been a few things that have come up when I was working with it that I wished it had. I now have 3 contributions to the project which we currently use!

Now that I’ve been so active with Robot Framework, I’ve been asked to join the list of active contributors which of course I will do, and happy to help :)

Now you may wonder how you can find things you can do?

  1. Begging :) Like when I was looking for a project to work for Clojure for my “vacation”.
  2. You could ask the maintainers if there is anything you can do to help.
  3. Something you need. When working with httparty, I was constantly referring to the examples directory to find something I knew was there. With Robot Framework, there was some functionality I needed.
  4. Look through the existing issues on a project to see if you can help.
  5. Keep checking the issues that come through on a project. One of the tickets I did for Robot Framework was something someone else requested.

Now, you found something you need or you think you can help with.

Ask on the chat channel explaining your need, they might said oh do it this way or its already there or may say, go ahead and submit a PR. You should always ask because you want to make sure you at least have a chance at being accepted.

When I saw someone ask for a feature on Robot Framework, I asked if he didn’t want to make a PR, that I would be happy to help. He said he didn’t have time and that I could do it. Come to think of it, I also asked on that Clojure ticket since someone had already completed half the work but didn’t finish. I asked if I could finish and the other party said sure go ahead! … which leads me to this..

It is just polite to ask :) I witnessed someone reporting a bug with a project, the maintainers said “oh thats a bug can you submit a PR?” and before my friend could act, someone not involved in the conversation submitted a PR and “took it”. Great, but he could have asked first. Needless to say, my friend still holds a grudge. So speak up first and be polite.

Look on the contributor guidelines for the project. Style guides, tests etc all make your PR go smoother.

You can use your open source contributions as “proof you know what you are doing” at your next job interview. In the past, when applying for Clojure jobs I talked about the PR I did for Clojure 1.8. Now I can use the PRs I did for Robot Framework. I even put them on my resume in an Open Source section!

I hope I’ve inspired you to go out there and submit some open source code (or some docs, like my submission to httparty). Worst they can say is No and you may even something in the process.

Cool Things in Clojure 1.9

I was glad to see Clojure 1.9 out because of Spec and some more things. But, was sad, because I can no longer say “Hey! I have a commit in the latest version of clojure!”. Course, that just gives me some motivation to find ways to help out in .. Clojure 2.0 ?? OMG. I don’t like to brag, but it was fun when this guy in my community (mostly he didn’t do Clojure) when he heard I had a patch in 1.8, come up to me and say “BADASS!!! WAY TO GO!!!”. I did have the help of Alex Miller and others and it was partly done when I took it over. It was really fun to be a small part of 1.8

Here are the things I think are cool (besides Spec) in Clojure 1.9:

Reader Syntax for Namespaced Maps

1
2
3
4
5
user=> (def data #:person{:name "Nola", :color "purple", :state "TX"})
#'user/data

user=> (keys data)
(:person/name :person/color :person/state)

Using #:ns-name{ key value key value } gives you an easy way to write out your map without prefixing each key. COOL.

Predicate Functions

This were put in mainly (I think) to support Spec, but in short, predicate functions (ending in ?) return true or false.

Some of them added in this patch are:

  • boolean?
  • int? pos-int? nat-int?
  • double?
  • ident?
  • qualified-keyword?

For example, Using remove with a collection will remove all values for which the predicate is true. So to remove all the postive integers from a vector:

1
2
user=> (remove pos-int? [-4 -2 4 1 0 -2 5])
(-4 -2 0 -2)

Then of course, in a spec:

1
2
3
4
5
6
7
8
user=> (s/def ::age pos-int?)
:user/age

user=> (s/conform ::age -16)
:clojure.spec.alpha/invalid

user=> (s/conform ::age 13)
13

Then lets remove everything not a qualified keyword, using in an anonymous function::

1
2
user=> (remove #(not (qualified-keyword? %))  [:name/nola :color/purple 1 5 0])
(:name/nola :color/purple)

Previous you could do the samething of course, but you’d have to write more code. I don’t know if you would use them much outside of spec, but if you do there they are there. Nice.

Atoms get new functions!

Ever wanted to give an atom a new value but also get back what the value was too?

1
2
3
4
user=> (def name (atom "Nola"))

user=> (reset-vals! name "Nick")
["Nola" "Nick"]

Instead of just reset! you now have reset-vals! returning the old value followed by the new (current) value. And this funtion’s cousin swap-vals!:

1
2
3
4
5
user=> (def size (atom 10))
#'user/size

user=> (swap-vals! size inc)
[10 11]

This function, swap-vals! takes an atom and a function and returns the new and the old value.

And before you bikeshed on the names of these functions, don’t bother, it has been done. I think they are good names :)

Brew Install

Not exactly a part of the Clojure 1.9 Release Notes but I think it bears mention along with this release is that for you Mac Users (like me!), you can now brew install clojure. I wrote about it early because I was so excited. Read the post for more details which also links to more documentation on it.

And also if you are an new Clojure dev, with Atom you can setup a decent Clojure environment with Atom. I recommend looking over the docs for parinfer to learn its magics, but once you get it you are good for life.

I hope you are excited about Clojure 1.9 and are using it in your projets :) It was a long time coming, but the best things are worth waiting for, right? :)

One-Liners in Clojure and Ruby

A quick post to start off the new year.

You and your co-workers can’t decide where to go for lunch?

1
2
 ruby -e "puts [:tacos, :chicken, :bbq].sample"
chicken
1
2
 clj -e "(rand-nth [:tacos :chicken :bbq])"
:tacos

Observations:

Ruby I had to tell it what to do with the output, to puts it. It didn’t automatically display like if I had done that line in the irb repl. Oh and don’t do what I did at first and forget the , (comma) :)

Clojure, since everything returns a value, I just had to call the function.

Have fun using code to make important decisions, like .. Lunch :)

Getting Started With Clojure Is Now Easier Than Ever - on a Mac

As of Dec 8, 2017 a brew recipe has been added to install Clojure with brew install clojure and it gives you commands clj and clojure.

To try it out, I created a file, test.clj with the following:

1
2
3
4
5
6
7
#!/usr/bin/env clojure

(println "Hello World")

(println (+ 1 2 3 4 5))

(println (clojure.string/upper-case "hello world"))

After you make it executable with:

1
chmod u+x test.clj

You can execute it:

1
2
3
4
▶ ./test.clj
Hello World
15
HELLO WORLD

A popular way to play with Clojure is to use a repl. You can start a repl, by using either clj or clojure without a file name

1
2
3
4
5
6
7
8
 clojure
Clojure 1.9.0
user=> (+ 1 2 3 4)
10
user=> (apply str [1 2 3 4])
"1234"
user=> (map #(* 2 %) [1 2 3 4])
(2 4 6 8)

I was curious about commands clojure and clj and found an explaination of how it works.

You can have a deps.edn file to include dependencies. At first I was confused on :mvn/ prefix, and thought how to include a library from clojars? So I looked farther and found that :mvn includes this group of locations (which includes clojars), as shown here.

1
2
3
{:mvn/repos
 {"central" {:url "https://repo1.maven.org/maven2/"}
  "clojars" {:url "https://repo.clojars.org/"}}}

to test this out, I created a directory and put a file deps.edn file with the following:

1
2
{:deps
 {anagrams {:mvn/version "4" }}}

Anagrams is a fun little library I found browsing clojars. It takes a list of words separated with \n and finds words that are anagrams of a word you give it. My example is a little contrived, but see it working:

Start the repl in that same directory, if needed it will download and install dependencies:

1
2
3
4
5
6
7
8
9
10
11
12
13
 clj
Clojure 1.9.0
user=> (require '[anagrams.core :as a])
nil

user=> (a/set-word-list! "cat\ntac\natc\ndog\ngod\ntaco")
"done"

user=> (a/anagrams-of "cat")
#{"tac" "atc" "cat"}

user=> (a/anagrams-of "dog")
#{"dog" "god"}

Cool, that’s fun :)

A new way to try out Clojure.. (on a mac at least). Pretty easy!

Data Structures in Clojure and Elixir: Structs and Records

Both Elixir and Clojure have a way to make a more “organized” entity, like a map but more structure. Elixir can easily have default values whereas with Clojure, it is not built in but you can create a helper functions to set some default values and create a record.

Elixir

A defstruct takes on the name of the defmodule it is in:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
iex(1)> defmodule User do
...(1)>   defstruct name: "", state: "", color: "green", size: "M"
...(1)> end
{:module, User,
 <<70, 79, 82, 49, 0, 0, 8, 60, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 0, 232,
   0, 0, 0, 22, 11, 69, 108, 105, 120, 105, 114, 46, 85, 115, 101, 114, 8, 95,
   95, 105, 110, 102, 111, 95, 95, 9, 102, ...>>,
 %User{color: "green", name: "", size: "M", state: ""}}

iex(2)> %User{name: "nola", state: "TX"}
%User{color: "green", name: "nola", size: "M", state: "TX"}

iex(30)> user.name
"nola"

iex(30)> user.color
"red"

Using an attribute that doesn’t exist:

1
2
3
4
iex(32)> user["country"]
** (UndefinedFunctionError) function User.fetch/2 is undefined (User does not implement the Access behaviour)
    User.fetch(%User{color: "", name: "nola", state: "TX"}, "country")
    (elixir) lib/access.ex:304: Access.get/3

Yeah don’t do that :) Accessing a key not defined is not good.

Clojure

First define a record with defrecord and a vector for the names of the attributes:

1
2
user=> (defrecord DataRecord [name state])
user.DataRecord

Then create a record

1
2
user=> (def data (->DataRecord "Nola" "TX"))
#'user/data

Access the values just like a map:

1
2
3
4
5
user=> (:name data)
"Nola"

user=> (:state data)
"TX"

Or create using a map:

1
2
user=> (map->DataRecord {:name "bob" :state "IL"})
#user.DataRecord{:name "bob", :state "IL"}

To create a Record that may have default values, create a helper function to build it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
user=> (defrecord Person [name state color size])
user.Person

user=> (defn create-person [{:keys [name state color size] :or {color "green" size "M" }}] (->Person name state color size))
#'user/create-person

user=> (def data (create-person {:name "bob" :state "TX"}))
#user.Person{:name "bob", :state "TX", :color "green", :size "M"}

user=> (:name data)
"bob"

user=> (:state data)
"TX"

user=>

I wrote about Clojure records awhile back, See more on records.

See my previous posts on Maps, Lists, Tuples, Vectors and Sets.

Data Structures in Clojure and Elixir: Maps

Elixir

Tuples used { } .. but maps use %{ }. You can use anything for a key.. here are string keys:

1
2
3
4
5
iex(1)> data = %{"name" => "nola", "color" => "red"}
%{"color" => "red", "name" => "nola"}

iex(4)> data["name"]
"nola"

If you use a string to set the value, obviously you need to use that to get the value.

Using atoms for keys is alot nicer for maps, the syntax is shorter and you can use .name to access the value

1
2
3
4
5
iex(2)> more_data = %{city: "Austin", state: "TX"}
%{city: "Austin", state: "TX"}

iex(5)> more_data.city
"Austin"

I don’t know why you would, but you can use integers as keys too:

1
2
3
4
5
iex(3)> yet_more = %{1 => "user 1", 2 => "user 2"}
%{1 => "user 1", 2 => "user 2"}

iex(12)> yet_more[1]
"user 1"

And, even a list as a key:

1
2
3
4
5
iex(8)> weird_keys = %{ [1, 2] => "one two", [3, 4] => "three four"}
%{[1, 2] => "one two", [3, 4] => "three four"}

iex(9)> weird_keys[[1,2]]
"one two"

I can’t think of a use case for that, but if you need it, then you can do it.

Some methods for maps:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
iex(6)> Map.keys(more_data)
[:city, :state]

iex(7)> Map.keys(data)
["color", "name"]

iex(13)> Map.keys(weird_keys)
[[1, 2], [3, 4]]

iex(14)> Map.values(weird_keys)
["one two", "three four"]

iex(25)> Enum.count(weird_keys)
2

Updating a value in a map

1
2
3
4
iex(15)> data = %{color: "red", name: "nola"}

(16)> updated = %{data | color: "pink" }
%{color: "pink", name: "nola"}

Using the | operator with either syntax for strings or atom keys (whichever you are using) ie `”color” => “pink”’.

But if the key does not exist, you will get an exception!

To add a new key to a map, you use Map.put_new:

1
2
3
4
iex(1)> data = %{color: "red", name: "nola"}

iex(2)> Map.put_new(data, :state, "TX")
%{color: "red", name: "nola", state: "TX"}

If the key was already there, the change is ignored.

To add a new key to a map reguardless if it was there before or not use:

1
2
3
4
5
6
7
8
iex(10)> data = %{color: "red", name: "nola"}
%{color: "red", name: "nola"}

iex(11)> more = Map.put(data, :color, "green")
%{color: "green", name: "nola"}

iex(12)> Map.put(more, :state, "TX")
%{color: "green", name: "nola", state: "TX"}

Still, the data is not actually changed but a copy is returned. The original is unchanged.

Clojure

1
2
3
4
5
user=> (hash-map :name "nola" :state "TX" :color "red")
{:color "red", :name "nola", :state "TX"}

user=> (array-map :name "nola" :state "TX" :color "red")
{:name "nola", :state "TX", :color "red"}

I was trying to figure out what the difference was and found this on stackoverflow

Array maps and hash maps have the same interface, but array maps have O(N) lookup complexity (i.e. it is implemented as an simple array of entries), while hash maps have O(1) lookup complexity.

If you notice in my code above, the hash-map has the keys in a different order than I created them in.

So, thats the difference.

Using the literal { } it produces an array-map.

1
2
user=> (class {:name "Nola" :state "tx"})
clojure.lang.PersistentArrayMap

Some functions for maps:

1
2
3
4
5
6
7
8
9
10
11
user=> (def data {:color "red", :name "nola", :state "TX"})
#'user/data

user=> (keys data)
(:color :name :state)

user=> (vals data)
("red" "nola" "TX")

user=> (count data)
3

Editing or adding new keys to a map:

1
2
3
4
5
user=> (assoc data :state "IL")
{:color "red", :name "nola", :state "IL"}

user=> (assoc data :food "tacos")
{:color "red", :name "nola", :state "TX", :food "tacos"}

Unlike Elixir, Clojure doesn’t care if this value is new or not when updating a map. It also doesn’t change the original data structure like Elixir.

Let me know if I am understanding these languages right. Next post will talk about Elixir Structs and Clojure Records.

See my post on Lists, Tuples, Vectors and Sets.

Data Structures in Clojure and Elixir: Sets

My last post talked about Lists, Tuples and Vectors comparing Elixir and Clojure … now lets cover a related topic.. sets.

When you think of set, think of Math Sets.

Elixir

There are two ways to create a Set in Elixir

Using the Pipe Operator and put:

1
2
set = MapSet.new |> MapSet.put("apple") |> MapSet.put("apple") |> MapSet.put("banana")
#MapSet<["apple", "banana"]>

Or use a List and pass that to MapSet.new

1
2
iex(1)> set = MapSet.new(["apple","orange","grape", "grape"])
#MapSet<["apple", "grape", "orange"]>

I purposely put duplicates when I created it to see what would happen. It quietly dismissed the duplicate value and made a collection of unique values.

Passing a list to new would be a good way to filter out duplicates in a collection as well as build a set more easily.

1
2
iex(3)> unique_values = MapSet.new(["apple","orange","grape", "grape"]) |> MapSet.to_list()
["apple", "grape", "orange"]

Some more functions for sets

1
2
3
4
5
iex(4)> fruit = MapSet.new(["apple","orange","grape"])
#MapSet<["apple", "grape", "orange"]>

iex(5)> MapSet.member?(fruit, "apple")
true

and subset

1
2
3
4
5
6
7
8
iex(4)> fruit = MapSet.new(["apple","orange","grape"])
#MapSet<["apple", "grape", "orange"]>

iex(6)> other_fruit = MapSet.new(["apple", "grape"])
#MapSet<["apple", "grape"]>

iex(8)> MapSet.subset?(other_fruit, fruit)
true

Read more on MapSet

Clojure

You have two ways to make a set in Clojure, using the set function and the #{} literal.

1
2
3
4
5
user=> (set [:apple :grape :orange :orange])
#{:orange :apple :grape}

user=> (set '(:apple :grape :orange :orange)
#{:orange :apple :grape}

We are converting a vector or a list to a set.

Also testing putting duplicates and as expected, the result is all unique values.

1
2
3
4
user=> (def taco-restaurants #{"torchys" "maudies" "torchys" "taco bell"})

IllegalArgumentException Duplicate key: torchys  clojure.lang.PersistentHashSet.createWithCheck (PersistentHashSet.java:68)
RuntimeException Unmatched delimiter: )  clojure.lang.Util.runtimeException (Util.java:221)

No, the error is not calling taco bell a restaurant, it is putting torchys twice. YIKES… the literal syntax does not like duplicates. If you think your values might have duplicates, put in vector first and pass to set.

Edit: Alex Miller pointed out, the reason we have an error it because it is an invalid set. Now I think it should error and since Elixir doesn’t have a literal syntax it is not exactly the same thing to compare. Thanks for pointing that out why it errors Alex :)

Some useful functions for sets:

1
2
3
4
5
6
7
8
user=> (def valid-actions #{:get :post})
#'user/valid-actions

user=> (contains? valid-actions :post)
true

user=> (contains? valid-actions :head)
false

Use a set when you need to see if a certain value is one of X. It is surprisingly not easy to do the same with checking membership in a list or vector :) Use a set, you don’t need duplicates anyways so it makes sense.

There is a Set namespace with functions for sets:

1
2
3
4
5
6
7
8
user=> (require '[clojure.set :as set])
nil

user=> (set/subset? #{:post} valid-actions)
true

user=> (set/subset? #{:head} valid-actions)
false

Next, we’ll cover Maps!