BrunoBonacci / rdt

RDT is a REPL-Driven Test library for Clojure
Apache License 2.0
12 stars 1 forks source link

check out `hara.test` #1

Closed zcaudate closed 3 years ago

zcaudate commented 3 years ago

hi @BrunoBonacci

You might want to check out https://github.com/zcaudate/hara/tree/master/src/hara/test as it was a simplified version of midje. There's no nesting => in blocks as the parser just splits the expressions into pairs.

There's only 2 or 3 really important files so you can probably extract them out and customise it the way you want. It's a much simpler codebase than Midje (15%) with about 80% of the functionality. Example usages are in the test/ directory:

https://github.com/zcaudate/hara/blob/master/test/hara/lib/jsoup_test.clj

test runner output:

Screen Shot 2021-08-23 at 11 11 36 am
BrunoBonacci commented 3 years ago

Hi Chris,

thanks very much for the feedback, I looked into hara.test and there are few things I can definitely reuse ;-)

When i refer to the nesting I'm NOT talking about the nesting of facts like Midje, I don't mind that one. What I don't like is the let nesting to build-up state.

This problem mostly appears on integration tests where you have to build up some state in order to be able to test something. There is a better example in the Readme Motivation, but here is the crux:

(fact "something"
  (let [a (create-something)]
    a => {some state}
    (let [b (create-child a)]
      b => {some other state}
      (let [c (create-subnode b)]
        c => {whatever}))))

Here a and b only serve to reach c which is the item I want to test, but adding few other checks along the way. Now if c is failing, and I want to try this on the REPL, I have to unpack/undo all the let binding into defs so that i can evaluate them individually. So I will need to write something like

(def a (create-something)) 
(def b (create-child a))
(def c (create-subnode b))
;; eval 1-by-1 and check the issue

This is really tedious, time-consuming and error-prone. So what I did with RDT is to use the latter as a test

(repl-test
  (def a (create-something)) 
  a => {some state}

  (def b (create-child a))
  b => {some other state}

  (def c (create-subnode b))
  c => {whatever})

And what RDT does, is to convert the latter form into the first form (with let), so that your tests are safe to run in a multi-thread environment. I think for tests, less structure is better, it is definitely more readable, and easier to work with.

As I said, this problem dominates in integration tests, where the test has to build up some state before it can verify something, such as start the test cluster, create queues, tables, items etc, before being able to test something.

I haven't seen anything in hara.test that would suggest a similar strategy. Have you come across a similar issue? how do you solve this problem?

Bruno

zcaudate commented 3 years ago

What I don't like is the let nesting to build-up state

I agree with that. hara.test doesn't do the let nesting - I just parse for the => arrow and split on left and right sides.

The non-nested syntax you wrote out for rdt is the only valid syntax for hara.test. I had thought that it would be a disadvantage when I started doing the library but found exactly what you found when needing more fine grained control over testing - The simpler and more explicit the better. What you are doing is pretty much what I'm doing - that's why I thought I'd leave a comment.

I've since add more features like tabular data, retesting but the parsing is more or less the same basic code.

zcaudate commented 3 years ago

one thing that you might want to consider is to have :setup and :teardown options to your blocks.

I added those to the meta section of the tag form:

ie:

^{;; *************
  ;;
  ;; TICKER FULL
  ;;
  ;; *************
  :refer statsnet.system.events-ticker/ticker-es-register :added "4.0"
  :setup [(l/rt:inner-restart)
          (config/setup-bench)
          (!.lua
           (u/<! DEBUG :es-handler ticker/ticker-es-connect)
           true)
          (ticker/ticker-redis-register)
          (ticker/ticker-redis-start)
          (ticker/ticker-es-register)]}
(fact "registers the ticker es service"
  ^:hidden

  ;;
  ;; DO INITIAL SETUP
  ;;

  (def -conn- (redis/test:connection {:port 17003}))
  (def -ds- (c/http-stream (str "http://127.0.0.1:"
                                  (:port (l/rt:inner :lua))
                                  "/eval/es")))

  (ws/connection-count ticker/TICKER_ES_KEY "event-stream")
  => 1

  (t/task-count ticker/TICKER_REDIS_KEY)
  => 1

  ;;
  ;; PUBLISH VIA REDIS
  ;;

  (h/req -conn- ["PUBLISH" (last (:form @ticker/TICKER_PUBLIC_CHANNEL))
                 "hello"])
  => pos?

  (h/req -conn- ["PUBLISH" (last (:form @ticker/TICKER_PUBLIC_CHANNEL))
                 "world"])
  => pos?

  ;;
  ;; CHECK WEBSOCKET OUTPUT
  ;;

  (Thread/sleep 1000)
  @(:events -ds-)
  => ["hello" "world"]

  (h/close -conn-))
zcaudate commented 3 years ago

You also may be interested in other testing tools I've done. - here is a video of my test/documentation workflow: https://www.youtube.com/watch?v=dO7eyFtDDOY.

BrunoBonacci commented 3 years ago

I'm still undecided on how to handle the :setup and :teardown. My current line of thinking is that I need probably two levels.

The reason I think I would need something at namespace level is to avoid setting up and tearing down things like localstack on each test which would be quite time consuming.

One idea I have is to try to automatically determine what needs to be torn down with a strategy similar to closeable-map using the list of def in the block.

I had a look a the video you shared, I have to say you built an amazing platform for yourself. You should consider talking a bit more about it.

zcaudate commented 3 years ago

In my current setup, I have a fact:global form that does that.

(fact:global
 {:setup [(l/rt:inner-restart)
          (def +port+ (:port (l/rt :lua)))]
  :teardown  [(l/rt:stop)]})

Here is the current code.test.compile namespace for reference.

I'm quite happy to open source the code management libraries shown on the video. The old code is in the hara repo. The new code has changed a lot but the ideas are pretty much the same so let me know which functionalities may be useful for you.

There are a couple of things that have made me a bit tentative/lazy about open sourcing (which I had been doing quite a lot in the past):

zcaudate commented 3 years ago

complete code.test files:

https://gist.github.com/zcaudate/31fdf5437d43db7af058bd88c684e41f

it differs from the old hara.test

BrunoBonacci commented 3 years ago

I agree with many of the above points. Especially in the Clojure ecosystem, it seems to be easier to rewrite rather than reuse. In regards to my libraries, I mostly open-source stuff that I build for a project and I want to reuse in other projects. So like yourself, most of my libraries are very specific to my own use-cases. safely for example, it was in use in many of my projects long before I decided to make it opensource, and like for hara, my libraries when used together, their combined value increases.

I definitely would be interested in understanding and exploring more what you mean when you say:

I'm using clojure now as a compiler rather than a runtime

I do organise online Clojure Meetups via the London Clojurians, if you want I'd like to organise a talk for you to explore better your system/platform/library/approach and understand a bit more about the ideas you have. If you want we can plan something for around December. What's the best way to get in touch with you?

Alternatively, we are also organising an online free conference called re:Clojure and we will issue a Call for Papers in the next few days.

Thank you for the insightful discussion and the hara.test code

zcaudate commented 3 years ago

@BrunoBonacci, that's great to know. A lot of great ideas have come out from London. You can contact me via (<my github username> at outlook dot com). Unfortunately, I'm a little busy to prepare a public talk at this stage as I'm months behind on things right now. It'd be great to chat if you have time. I'm interested in what's happening over in Europe at the moment, especially in these strange times.


I'm mainly using clojure as a transpiler ie.

(defn.js isPlainObject
  "checks if object is a plain object"
  [val]
  (return (== (-/typeString val) "Object")))

gets transformed to javascript code

function isPlainObject(val){
  return typeString(val) == "Object";
}

It's kind of like cljs but it's a braindead transpile on the clj side. These is no immutable datastructures, only what the native language provides. Similar project exist - https://github.com/timothypratley/rustly but the difference is that I can customise the grammer. The native toolchain is used for builds so in the case of js, it's npm and webpack. There's pros and cons to this approach, the pros being the ability to use macros and to target exotic languages. The cons are obvious things like language/type safety but I'm more interested in performance at the moment so it's not a huge issue.

zcaudate commented 3 years ago

Especially in the Clojure ecosystem, it seems to be easier to rewrite rather than reuse.

I think that's the curse of being able to customise code exactly the way you want it - but it's still infinitely better than writing javascript.

zcaudate commented 3 years ago

I just found this:

https://github.com/echeran/kalai

which is similar to what I'm doing (but the code is pretty unmaintainable imo).