0

I have a greeting component that updates the time of day. Depending on the time of day the greeting will print

  • Good Morning, Jane
  • Good Afternoon, Jane
  • Good Evening, Jane

I have function called getTimeOfDay and test that this function is working. If you're interested view my tests here.

In my component I have a timer that checks every minute to see if the time of day message should update.

const [date, setDate] = useState(new Date())
const timeOfDay = getTimeOfDay(date)

useEffect(() => {
  const timer = setInterval(() => {
    setDate(new Date())
  }, 60000)

  return () => {
    clearInterval(timer)
  }
}, [])

I have been going back and forth on whether to test that this message changes correctly as the time passes. I know that testing implementation details is bad practice and was not sure if this was an implementation detail or a feature that should be tested.

If this is something I should test, I can't seem to easily implement a timer in jest that checks the message and then speeds up the timer 8 hours. What are your thoughts?

Jamie
  • 1,909
  • 3
  • 22
  • 47
  • Seems like [you can set a mock date in Jest](https://stackoverflow.com/questions/29719631/how-do-i-set-a-mock-date-in-jest). Also found [this article](https://codewithhugo.com/mocking-the-current-date-in-jest-tests/). So, it seems possible to test this for different times of the day. – VLAZ Mar 18 '21 at 17:27
  • 1
    Treat time as a dependency (see one way [here](https://stackoverflow.com/a/65130857/3001761)) then it’s easy to write multiple tests with different times - morning, afternoon, evening, getting into more detail around the transition points if you like. – jonrsharpe Mar 19 '21 at 09:00
  • So I think my first concern is whether this is an implementation detail or not. – Jamie Mar 19 '21 at 13:46
  • @Jamie it's a bit blurry in this case. In pure technicality, it *is* an implementation detail. If you treat the function as a black box, you don't know whether it uses `Date` or not. On the other hand, if it *always* returns "Good evening, Jane" then that's a faulty implementation. Those are cases that need to be tested to ensure correctness. So, from a pragmatic standpoint, it should be possible to test if the function behaves correctly for different times. jonsharpe's suggestion to externalise the dependency is the perfect middle ground - it's *not* an implementation detail any more. – VLAZ Mar 22 '21 at 09:26

2 Answers2

0

You can change date for testing purpose overriding Date inside your tests using mock function.

    Date.now = jest.fn(() => new Date( "2021-01-01 8:50:00"))
    
    const result = getTimeOfDay(switchToMorning)
    expect(result).toEqual('Morning')
Kishieel
  • 1,811
  • 2
  • 10
  • 19
  • Maybe it is because switchToMorning which has hard coded date when created. When you want to use above method inside tested funciton you should just use ```new Date()```. – Kishieel Mar 23 '21 at 15:41
0

My final tests are as followed:

describe('Greeting', () => {
  const name = 'Jack'
  
  it('renders component as expected', () => {
    const wrapper = mount(<Greeting name={name} /> )
    expect(wrapper.text().includes(name)).toBe(true)
  })

  it('Should update message after time', () => {
    jest.useFakeTimers()
    setMockDate(new Date('Feb 22, 2021 11:59:00'))
    const wrapper = mount(<Greeting name={name} />)

    const greetingText = wrapper.text()
    setMockDate(new Date('Feb 22, 2021 12:00:00'))
    jest.advanceTimersByTime(60000)
    expect(wrapper.text()).not.toBe(greetingText)
  })

  it('Should clear interval on unmount', () => {
    const spyOnSetInterval = jest.spyOn(window, 'setInterval')
    const spyOnClearInterval = jest.spyOn(window, 'clearInterval')
    spyOnSetInterval.mockReturnValueOnce((33 as unknown) as NodeJS.Timeout)
    const wrapper = mount(<Greeting name={name} />)

    wrapper.unmount()
    expect(spyOnClearInterval).toHaveBeenCalledWith(33)
  })
})

I created a helper function to mock the date. The final version is:

/**
 * @param {Date} expected
 * @returns {Function} Call to remove Date mocking
 */
const setMockDate = (expected: Date): AnyObject => {
  const RealDate = Date

  function MockDate(mockOverride?: Date | number) {
    return new RealDate(mockOverride || expected)
  }

  MockDate.now = () => expected.getTime()
  MockDate.prototype = RealDate.prototype

  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  global.Date = MockDate as any

  return () => {
    global.Date = RealDate
  }
}

export default setMockDate
Jamie
  • 1,909
  • 3
  • 22
  • 47