Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to properly create object for testing? #482

Closed
3 tasks done
JuanCaicedo opened this issue Mar 10, 2022 · 5 comments
Closed
3 tasks done

How to properly create object for testing? #482

JuanCaicedo opened this issue Mar 10, 2022 · 5 comments

Comments

@JuanCaicedo
Copy link

JuanCaicedo commented Mar 10, 2022

Hi Justin and team! Hope you're all doing well 馃槃

Description

I would like to create a dummy object that satisfies types in my test and allows me to target different cases in my tests.

Issue

Say I have a type defined

type Dog = {
  name: string
  age: number
}

And I have a function that depends on this type

function hasGreatName(dog: Dog): boolean {
  if (dog.name === 'Spot') {
     return true
  }
  return false
}

I would like to satisfy the Dog type in my tests, and also assert a specific behavior.

These are the two ways I imagined doing that

describe('hasGreatName', () => {
  it('returns true if the dog is named Spot', () => {
  
    // fails TS compilation with the following error
    // > Property 'age' is missing in type '{ name: string; }' but required in type 'Dog'.
    const dog1 = td.object<Dog>({
      name: 'Spot'
    })
    expect(hasGreatName(dog1)).toEqual(true)
    
    const dog2 = td.object<Dog>()
    // Throws a TD warning at run time
    // > Warning: testdouble.js - td.replace - property "name" [test double for ".name"] (Function) was replaced with "Spot", which has a different type (String).
    td.replace(dog2, 'name', 'Spot')
    expect(hasGreatName(dog2)).toEqual(true)
  }) 
})

The part about name being a Function really confuses me 馃槄

Let me know if I'm doing something wrong, or if there's something to address in the lib that I could help out with! Thanks y'all 馃榾

Environment

  • node -v output: v16.14.0
  • npm -v (or yarn --version) output: 8.3.1
  • npm ls testdouble (or yarn list testdouble) version: 3.16.4

Repl.it Notebook

https://replit.com/@JuanCaicedo1/TDTestObjects2

@JuanCaicedo
Copy link
Author

I know it would be possible to fix the TS compilation on dog1 by adding an age, but in my code that's specifically what I'm trying to avoid 馃榾 The object I want to fake is a type from a third party and I would need to define a ton of things in order to satisfy the type

@searls
Copy link
Member

searls commented Mar 14, 2022

The literal reason you're seeing this is probably because td.object generates functions, not static values, of the names passed to it.

In ~13 years (yikes) of practicing TDD on-and-off-again with mocks, I have become pretty fixed in my conviction that values (stuff with name and age like your Dog) should have only elucidative behavior that describes the data they hold, and not any actual feature/business logic. As a result, I never mock them, I always instantiate real ones in my tests. If it's too hard to instantiate those values, then it usually means they're too complicated and I take that feedback to simplify or break down. So for that reason, I'd recommend just instantiating a dog and passing it.

Second, I only fake dependencies that expose some kind of feature/business functions that do the work needed by the subject under test. That means I'd be more likely to replace a module that had a blowDry(dog) function that either transformed or mutated the dog and stub its return value or verify it was called, respectively.

@searls searls closed this as completed Mar 14, 2022
@JuanCaicedo
Copy link
Author

Hi Justin! Thanks for the guidance 馃槃

Do you think this API would make sense? If so I think that would unlock my use case, even if it's not the best practice 馃槄

const partialDog: Partial<Dog> = {
  name: 'Spot'
}
const mockDog = td.object<Dog>(partialDog) 
mockDog.name // 'Spot'
mockDog.age // undefined

In case that's not possible, I'll add a few questions on the rest of your answer 馃榾 Thanks!

@JuanCaicedo
Copy link
Author

The impression I get is the above already works at runtime. If that seems okay, then perhaps it would be possible to change main/index.d.ts#L224-L232 to the following

/**
 * Create a fake object that is deep copy of the given object.
 *
 * @export
 * @template T
 * @param {Partial<T>} object Object to copy.
 * @returns {DoubledObject<T>}
 */
export function object<T>(object: Partial<T>): DoubledObject<T>;

Let me know what you think of that 馃榾 I can try it against my tests to see how it would work

@searls
Copy link
Member

searls commented Mar 16, 2022

This might technically work, but I'm still fuzzy on the problem being solved here. As far as I can tell there are at least two issues:

  1. Faking a value object using td.object that contains some number of static properties that must be set for the backing typescript type to be considered valid. As I wrote above, faking value objects isn't IMO a good idea, and instead real value objects should be used whenever possible. (And to the extent that values & application behavior are inter-mingled, I'd take that as a cue to disentangle them.) As a result, I'm not particularly motivated to make any changes to support that usage

  2. Trying to pass a partial thing to td.object so each and every property doesn't have to be satisfied before the object is copied and a fake is returned. (I don't use Typescript, but I imagine that's what Partial<T> would be doing, and is a built-in type of the language.) To me, this seems to violate type safety -- any time I'm creating and passing a not-quite-valid version of an object to a function, the primary benefit of a type system is the compiler telling me "yo that Dog over there isn't valid". I would think (and when I use mock objects in typed languages, I personally practice) that I'd actually want my tests to be a way to flesh out my types, which requires that all instances be valid and complete.

I'm speculating a lot above, but am I off base here? Is td.object in particular different from other functions in the library such that it only makes sense in this case or would you apply this to every td function?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants