Hacker News new | ask | show | jobs
by randomdata 805 days ago
Per the dictionary, mock is defined as "not authentic or real, but without the intention to deceive."

As that applies to software, a mock is a fully-fledged implementation of an interface, but not the implementation you use under normal use. For example, a mock might be an in-memory database where you normally use a persisted database. Not the real implementation, but does not try to deceive – it is equally functional.

Mockery appears to be an assertion library that, bizarrely, moves the assertions into interface implementation. What purpose does it serve? You are going to end up testing implementation details by using that, which is a horrible road to go down.

1 comments

Mockery is equally function in the sense that it implements the interface it targets.

I wouldn't describe swapping a persisted DB for an in-memory DB mocking personally.

> Mockery is equally function in the sense that it implements the interface it targets.

Consider a function with a key/value database dependency:

  type DB interface {
   Get(key string) string
   Set(key string, value string)
  }

  func ContrivedFunction(db DB) string {
   db.Set("key", "value")
   return db.Get("key")
  }
And let's test it using mockery:

  func TestContrivedFunction(t *testing.T) {
   db := mocks.NewDB(t)
   if ContrivedFunction(db) != "value" {
    t.Error("unexpected result")
   }
  }
Code looks reasonable enough, but then... Epic failure. This should work, at least it should if `db` is a mock. But it does not work. `db` deceived us. Clearly not a mock in any sense of the word.

But, okay. It is still something. We will play its game:

  func TestContrivedFunction(t *testing.T) {
   db := mocks.NewDB(t)
   db.Mock.On("Set", "key", "value").Return()
   db.Mock.On("Get", "key").Return("value")
   if ContrivedFunction(db) != "value" {
    t.Error("unexpected result")
   }
  }
Wonderful. We're back in business. Tests are passing and everything is sunshine and rainbows.

But now, the contrived project manager just called and, for contrived reasons, would like to change the organization of record keys:

  func ContrivedFunction(db DB) string {
   db.Set("ns:key", "value")
   return db.Get("ns:key")
  }
Ah, fuck! The test just broke again. But it shouldn't have. The utility of ContrivedFunction – that which is under test – hasn't changed one bit. The DB implementation should have been able to handle this just fine. The DB implementation used in production handles this just fine. This mockery tool is fundamentally broken.

But not only is it broken, it doesn't seem to serve a purpose. Why would you even use it?

Mocks are especially useful when you want to test code heavy with dependency injection.

I like to say that normal tests where you test by passing in params and assert the result is outside in testing.

Mocking is inside out test. Follows naturally from dependency inversion of di. You want to assert your function is making the right calls/params to it's dependencies interfaces from inside.

In your contrived example there is not much value in knowing whether "ns:key" or "key" was used. But if you those are params to some external RPC suddenly this actually becomes pretty useful.

Providing working fakes for every dependency isn't always realistic.

Should I really spend time building a fake of Stripe. Or should I just assert the request I am making to it are the expected ones.

TLDR: I only saw the value of mocks when testing DI server code with many external service dependencies.

> Providing working fakes for every dependency isn't always realistic.

I don't know what a fake is. Is that what mockery gives you? Per the dictionary, fake is defined similar to mock, but without the no deception condition, so I suppose that adds up.

There are also stubs, which is defined as something that is truncated or a part of. Which, as it pertains to software, is an implementation that implements some kind of bare minimum to satisfy the interface – often returning canned responses, for example. Mockery arguably also fits here, except the assertion part, which is something else. But I guess that's where fake comes in to draw that differentiation?

> But if you those are params to some external RPC suddenly this actually becomes pretty useful.

Sure, a stub might check the inputs and return an error if some condition is not met, without needing to implement the service in full. This remains true to what the real service would also do.

But that's not what mockery does. It just blows up spectacularly if something wasn't right. That doesn't really make any sense. That is now how the real implementation works. Not only that, but in the case of mockery, its documentation advises that you put the dependency logic in the test. How silly is that? Now when you replace Stripe with Line all your tests are broken. If you used a stub, you merely change the stub to match and you're good to go. This way the tests remain pure, as they need to as they are the contract you make with your users. Changing the contract is unacceptable.

And for all that, it doesn't seem to serve any purpose. But we did ask the other guy for a concrete example (i.e. code) to show where one would want to use it. Looking forward to it.

Here, I'll provide an example I generated with chatgpt with some prompting.

    type Server struct {
        Payment         services.PaymentService
        Storage         services.StorageService
        Quota           services.QuotaService
        Auth            services.AuthService
        Notification    services.NotificationService
    }

    // TransactionRequest is the request structure for a purchase
    type TransactionRequest struct {
        UserID    string  `json:"user_id"`
        AuthToken string  `json:"auth_token"`
        ProductID string  `json:"product_id"`
        Price     float64 `json:"price"`
        Currency  string  `json:"currency"`
        File      []byte  `json:"file"`
        Key       string  `json:"key"`
    }

    // TransactionResponse is the response structure after processing a transaction
    type TransactionResponse struct {
        Status  string `json:"status"`
        Message string `json:"message"`
    }

    // HandleTransaction handles the incoming transaction request
    func (s *Server) HandleTransaction(w http.ResponseWriter, r *http.Request) {
        var req TransactionRequest
        if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
            http.Error(w, "invalid request", http.StatusBadRequest)
            return
        }

        // Authenticate user
        userID, err := s.Auth.Authenticate(req.AuthToken)
        if err != nil {
            http.Error(w, "authentication failed", http.StatusUnauthorized)
            return
        }

        // Check and update quota
        allowed, err := s.Quota.CheckQuota(userID)
        if err != nil || !allowed {
            http.Error(w, "quota exceeded", http.StatusForbidden)
            return
        }
        s.Quota.UpdateQuota(userID, 1)  // Assume updating quota by 1 unit per transaction

        // Process payment
        transactionID, err := s.Payment.Charge(req.Price, req.Currency, "payment-token")
        if err != nil {
            http.Error(w, "payment failed", http.StatusInternalServerError)
            return
        }

        // Upload file to S3
        fileURL, err := s.Storage.Upload(req.File, userID + "-files", req.Key)
        if err != nil {
            http.Error(w, "file upload failed", http.StatusInternalServerError)
            return
        }

        // Send notification
        notificationMessage := "Your purchase has been processed successfully."
        s.Notification.SendNotification(userID, notificationMessage)

        response := TransactionResponse{
            Status:  "success",
            Message: "Transaction processed successfully: " + transactionID + " File uploaded to: " + fileURL,
        }
        w.Header().Set("Content-Type", "application/json")
        json.NewEncoder(w).Encode(response)
    }
It is still contrived obviously. But what I would like to show what happens if use stubs. You could pass anything into the dependency parameters and the test would pass. Mock allows you to "lock" in the implementation. Its tightly coupled yes but sometimes that is exactly what you want.

In this example that would be the userId, the bucket name, the key.

Mock allows you inject faults in the test setup. Lets say I want to test the error handling logic. I could do it with multiple types of stubs but if you squint its starting to look the same.

On the topic of Fake vs Stub vs Mock. Real or Fake is the ideal solution here but not always viable due to time/practical constraints. Stubs can be useful but don't give you fine grained control.

> You could pass anything into the dependency parameters and the test would pass.

I don't follow. Under no circumstance would the test pass under invalid inputs, be it whether you use mocks, stubs, or even mockery (fakes?).

- Obviously it cannot pass while using mocks – a mock matches the real thing to every last detail. If it will fail while using the real thing, it will fail here as well.

- Obviously it cannot pass while using stubs – a stub matches the real thing, partially, to at least the minimum amount necessary for the sake of testing. If it will fail while using the real thing, it will fail here as well.

- Obviously it cannot pass while using mockery – mockery does not match the real thing in any way, but does require you to specify matching inputs with failure when they don't match. If it will fail while using the real thing, it will fail here as well.

I don't know what you are imagining, but I'm not convinced it is a thing. I mean, you could make it a thing - you can do anything your little heart desires, but there would be no reason to ever make it a thing.

Maybe we need some concrete tests to better illustrate what you are talking about?

What's the alternative? It's not always easy or practical to reach for "alternative DB implementation" because in most cases that simply doesn't exist.
Without knowing when mockery is useful, it is impossible to suggest an alternative. There may not be a viable alternative in some cases. That is why I ask. Perhaps you can give us a concrete example to work with, even if contrived? Where have you found mockery to be useful?

The above example, of course, isn't a good one as you can inject an in-memory database (i.e. a hash map) with far less effort than running the mockery tool. But, you are quite right that not all situations are as simple.

If I recall correctly, rsc says to just buckle up and provide a working implementation no matter how hard or how much work it requires, but that's easy for someone who works for Google, in a prestigious position at that, to say. We don't all live in the same lap of luxury. I grant some pragmatism here.

What have you got?